Pi‑Web Context Menu Extension Hook: How Downstream Integrations Work
Pi‑Web exposes a cancelable custom DOM event named pi‑web:session‑row‑contextmenu that lets extensions add, modify, or completely replace the context menu for session rows without any compile‑time coupling to the core application.
The agegr/pi-web repository implements a loosely coupled extension system for the session sidebar's context menu. Rather than exposing a rigid plugin API, it fires a standard browser CustomEvent that downstream code can intercept. This approach keeps the core UI lightweight while giving integrations full control over the user experience.
How the Extension Hook Is Architected
The Core Event Contract
All communication flows through a single event type defined in lib/session-row-context-menu.ts. The SessionRowContextMenuDetail interface carries everything an extension needs:
| Property | Purpose |
|---|---|
id |
Unique identifier for the session row |
path |
Absolute filesystem path to the session file |
cwd |
Working directory of the session |
name? |
Optional display name |
clientX / clientY |
Click coordinates for positioning custom UI |
refresh |
Callback to force the menu to re‑render |
The dispatchSessionRowContextMenu() helper creates a cancelable CustomEvent and returns a boolean indicating whether any listener claimed the menu:
// lib/session-row-context-menu.ts
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);
}
A return value of true means event.preventDefault() was called—a downstream listener has taken full responsibility.
Where the Hook Fires
In components/SessionSidebar.tsx, each session row attaches a native onContextMenu handler. The flow is:
- Build the
SessionRowContextMenuDetailpayload from row data - Call
dispatchSessionRowContextMenu() - If the result is
false, render Pi‑Web's default menu - If
true, skip rendering—the extension owns the experience
This logic lives entirely in the presentation layer. No business logic changes are required to support new integrations.
Downstream Integration Patterns
Pattern 1: Augmenting the Native Menu
Extensions can push items into a shared array and trigger a refresh. The UI merges these contributions before rendering.
// src/plugins/my-plugin.ts
import { SESSION_ROW_CONTEXT_MENU_EVENT, SessionRowContextMenuDetail } from '@/lib/session-row-context-menu';
window.addEventListener(SESSION_ROW_CONTEXT_MENU_EVENT, (e) => {
const ev = e as CustomEvent<SessionRowContextMenuDetail>;
const { path, clientX, clientY } = ev.detail;
extraMenuItems.push({
label: 'Open in external editor',
click: () => window.open(`my-editor://open?file=${encodeURIComponent(path)}`),
});
ev.detail.refresh();
});
Pattern 2: Complete Menu Replacement
Call event.preventDefault() and render your own DOM element at the supplied coordinates.
window.addEventListener(SESSION_ROW_CONTEXT_MENU_EVENT, (e) => {
const ev = e as CustomEvent<SessionRowContextMenuDetail>;
const menu = document.createElement('div');
menu.style.cssText = `position:absolute;left:${ev.detail.clientX}px;top:${ev.detail.clientY}px`;
menu.className = 'custom-context-menu';
menu.innerHTML = `
<button data-action="delete">Delete Session</button>
<button data-action="duplicate">Duplicate Session</button>
`;
document.body.appendChild(menu);
const cleanup = () => {
menu.remove();
document.removeEventListener('click', cleanup);
};
document.addEventListener('click', cleanup);
e.preventDefault(); // Claim the menu
});
Pattern 3: Async Data Loading
The refresh callback enables asynchronous workflows. Fetch remote data, update the menu state, then re‑render.
window.addEventListener(SESSION_ROW_CONTEXT_MENU_EVENT, async (e) => {
const ev = e as CustomEvent<SessionRowContextMenuDetail>;
const tags = await fetch(`/api/tags?session=${ev.detail.id}`).then(r => r.json());
extraMenuItems.push({
label: `Latest tag: ${tags[0]}`,
action: () => applyTag(tags[0]),
});
ev.detail.refresh();
});
Key Source Files for Extension Developers
| File | What It Contains |
|---|---|
lib/session-row-context-menu.ts |
Event name constant, SessionRowContextMenuDetail interface, and dispatchSessionRowContextMenu() helper |
components/SessionSidebar.tsx |
UI component that triggers the hook and respects cancellation |
components/ExtensionWidgets.tsx |
Reference implementation showing how Pi‑Web structures refreshable UI extensions |
lib/types.ts |
Type definitions for extension‑contributed items |
Summary
- Pi‑Web's context menu extension hook uses a standard DOM event (
pi‑web:session‑row‑contextmenu) for zero‑dependency integrations - Downstream code receives row metadata, click coordinates, and a
refreshcallback viaSessionRowContextMenuDetail - Calling
event.preventDefault()claims the menu and suppresses Pi‑Web's native UI - All integration points are runtime‑discoverable—extensions load via the plugin system and attach listeners without recompiling core code
Frequently Asked Questions
How does Pi‑Web know when to skip its default context menu?
The dispatchSessionRowContextMenu() function in lib/session-row-context-menu.ts returns true when any listener invokes event.preventDefault(). The SessionSidebar component checks this return value and renders its built‑in menu only when the result is false.
Can multiple extensions modify the same context menu?
Yes. Multiple listeners can attach to SESSION_ROW_CONTEXT_MENU_EVENT. Each can push to shared state arrays or call refresh(). However, only one listener should call event.preventDefault() if they want to replace rather than augment the menu.
What coordinate system do clientX and clientY use?
These are standard viewport coordinates from the underlying MouseEvent. Position absolutely‑placed custom menus using these values directly, or transform them if your UI uses a different coordinate space.
Is there compile‑time type safety for extension authors?
Yes. Extensions can import SessionRowContextMenuDetail from @/lib/session-row-context-menu to get full TypeScript intellisense for the payload shape, even though the event itself is dispatched and received at runtime.
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 →