# How Session Context Menu Integration Works for Downstream Consumers in Pi‑Web

> Discover how Pi-Web session context menu integration empowers downstream consumers to customize menus via DOM events without altering core UI code. Learn more today!

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-08-17

---

**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`](https://github.com/agegr/pi-web/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/agegr/pi-web/blob/main/components/SessionSidebar.tsx)) invokes the dispatcher whenever a user right‑clicks a session row:

```tsx
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:

```javascript
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:

```javascript
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`](https://github.com/agegr/pi-web/blob/main/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.