How Session Context Menu Integration Works for Downstream Consumers in Pi‑Web
Pi‑Web exposes session row right‑clicks through a cancelable custom DOM event that downstream consumers can intercept to replace or augment the context menu without modifying core UI code.
The agegr/pi-web repository implements a lightweight extension point for session context menu integration that allows external scripts and browser extensions to customize user interactions. By dispatching a standard CustomEvent with detailed session metadata, the application enables downstream consumers to hook into right‑click actions on session rows while maintaining clean separation from the sidebar component internals.
The Custom Event Architecture
All context menu plumbing resides in lib/session-row-context-menu.ts. This module exports a strongly typed CustomEvent implementation that carries the full context of the session row being clicked.
Event Contract and Type Safety
The event identifier and payload contract are defined as follows:
export const SESSION_ROW_CONTEXT_MENU_EVENT = "pi-web:session-row-contextmenu";
export interface SessionRowContextMenuDetail {
id: string; // Session UUID
path: string; // Absolute `.jsonl` file path
cwd: string; // Working directory of the session
name?: string; // Optional user‑defined name
clientX: number; // Click coordinate (for UI positioning)
clientY: number; // Click coordinate (for UI positioning)
refresh: () => void; // Callback to trigger a UI refresh after handling
}
/* Make the event type visible on `window`. */
declare global {
interface WindowEventMap {
"pi-web:session-row-contextmenu": CustomEvent<SessionRowContextMenuDetail>;
}
}
The global interface augmentation ensures TypeScript aware consumers receive full IntelliSense when attaching listeners to window.
The Dispatcher Implementation
The dispatchSessionRowContextMenu function constructs the event with cancelable: true and returns a boolean indicating whether a listener claimed the interaction:
export function dispatchSessionRowContextMenu(
detail: SessionRowContextMenuDetail,
target: EventTarget = window,
): boolean {
const event = new CustomEvent<SessionRowContextMenuDetail>(
SESSION_ROW_CONTEXT_MENU_EVENT,
{ cancelable: true, detail },
);
return !target.dispatchEvent(event);
}
- Return value semantics – Returns
truewhen a synchronous listener callsevent.preventDefault(), signaling that the downstream consumer has suppressed the native menu. Returnsfalsewhen the event is dispatched without cancellation.
UI Integration in the Session Sidebar
The SessionSidebar component (located in components/SessionSidebar.tsx) invokes the dispatcher whenever a user right‑clicks a session row:
const handled = dispatchSessionRowContextMenu({
id: session.id,
path: session.path,
cwd: session.cwd,
name: session.name,
clientX: e.clientX,
clientY: e.clientY,
refresh: () => loadSessions(false, true),
});
if (handled) {
e.preventDefault();
}
If handled is true, the component prevents the browser’s default context menu, allowing the downstream consumer to render custom UI. If false, Pi‑Web proceeds with its built‑in menu.
Implementing Downstream Consumers
Any script running in the same browser context can listen for the event on window. Below are two common integration patterns.
Replacing the Native Menu
To completely override the context menu, call preventDefault() and render your own interface:
window.addEventListener("pi-web:session-row-contextmenu", (e) => {
const { id, path, cwd, clientX, clientY, refresh } = e.detail;
// Render custom floating menu
const menu = document.createElement("div");
menu.style.position = "fixed";
menu.style.left = `${clientX}px`;
menu.style.top = `${clientY}px`;
menu.innerHTML = `
<button>Archive ${id}</button>
<button>Open in ${cwd}</button>
`;
document.body.appendChild(menu);
// Cleanup and refresh Pi‑Web list after action
menu.querySelector("button").onclick = async () => {
await archiveSession(id);
refresh(); // Signal Pi‑Web to reload sessions
menu.remove();
};
e.preventDefault(); // Suppress Pi‑Web’s default menu
});
Augmenting Without Replacement
For analytics or passive enhancements, listen without canceling:
window.addEventListener("pi-web:session-row-contextmenu", (e) => {
analytics.track("context_menu_opened", {
sessionId: e.detail.id,
cwd: e.detail.cwd,
timestamp: Date.now(),
});
// Do not call preventDefault() – Pi‑Web shows its native menu
});
Multiple listeners can coexist; however, only the first synchronous caller of preventDefault() will mark the event as handled.
Contract Validation and Testing
The behavior of the dispatcher is enforced by lib/session-row-context-menu.test.mjs. These tests verify:
- No listener –
dispatchSessionRowContextMenureturnsfalse, allowing the default menu. - Listener present (not cancelling) – Still returns
false, confirming delivery. - Listener cancels – Returns
truewhenevent.preventDefault()is invoked.
This test coverage guarantees that downstream consumers can rely on the cancellation contract across Pi‑Web releases.
Summary
- Pi‑Web dispatches the custom event
"pi-web:session-row-contextmenu"whenever a user right‑clicks a session row. - The event
detailobject implementsSessionRowContextMenuDetail, providingid,path,cwd, optionalname, click coordinates, and arefreshcallback. - The
dispatchSessionRowContextMenufunction returnstrueonly when a listener cancels the event, which suppresses the built‑in context menu. - Downstream consumers attach listeners to
windowto customize menus or track interactions without modifying React components. - Unit tests in
lib/session-row-context-menu.test.mjsensure the cancellation contract remains stable.
Frequently Asked Questions
What is the exact event name used for session context menu integration?
The constant SESSION_ROW_CONTEXT_MENU_EVENT defined in lib/session-row-context-menu.ts holds the string value "pi-web:session-row-contextmenu". Consumers should reference this constant when attaching listeners to avoid typos.
How does a downstream consumer prevent Pi‑Web from showing its default context menu?
Inside the event listener, call event.preventDefault(). This marks the CustomEvent as canceled, causing dispatchSessionRowContextMenu to return true. The SessionSidebar component checks this return value and skips its own e.preventDefault() and native menu rendering.
What data is available to downstream consumers when the context menu event fires?
The detail property of the event contains a complete SessionRowContextMenuDetail payload: the session id, absolute file path, working directory cwd, optional display name, pointer coordinates (clientX, clientY), and a refresh function that triggers a reload of the session list when invoked.
Can multiple downstream consumers listen to the same session context menu event?
Yes. Because the event dispatches on window, any number of listeners can observe it simultaneously. However, the first listener to synchronously invoke event.preventDefault() determines the handled state. Subsequent listeners can check event.defaultPrevented to determine whether another consumer has already claimed the menu.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →