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 true when a synchronous listener calls event.preventDefault(), signaling that the downstream consumer has suppressed the native menu. Returns false when 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 – dispatchSessionRowContextMenu returns false, allowing the default menu.
  • Listener present (not cancelling) – Still returns false, confirming delivery.
  • Listener cancels – Returns true when event.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 detail object implements SessionRowContextMenuDetail, providing id, path, cwd, optional name, click coordinates, and a refresh callback.
  • The dispatchSessionRowContextMenu function returns true only when a listener cancels the event, which suppresses the built‑in context menu.
  • Downstream consumers attach listeners to window to customize menus or track interactions without modifying React components.
  • Unit tests in lib/session-row-context-menu.test.mjs ensure 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →