From 6eb5e47aafaf9e9b4989e83b99743a1758f701a3 Mon Sep 17 00:00:00 2001 From: enan3003 Date: Thu, 13 Aug 2026 21:59:15 -0500 Subject: [PATCH] feat: label sessions by runtime --- extension/src/claude-runtime.ts | 1 + extension/src/codex-runtime.ts | 1 + extension/src/codex-session-watcher.ts | 1 + extension/src/pi-runtime.ts | 2 +- extension/src/pi-session-watcher.ts | 1 + extension/src/protocol.ts | 1 + extension/src/session-runtime.ts | 2 ++ extension/src/session-watcher.ts | 1 + extension/test/pi-session-watcher.test.ts | 1 + scripts/relay.ts | 17 +++++++++++------ scripts/session-command-palette.test.ts | 8 +++++--- .../session-command-palette.tsx | 15 +++++++++++++++ web/lib/bridge-types.ts | 1 + web/lib/session-search.ts | 2 +- 14 files changed, 43 insertions(+), 11 deletions(-) diff --git a/extension/src/claude-runtime.ts b/extension/src/claude-runtime.ts index 7831a46..ca5b4b1 100644 --- a/extension/src/claude-runtime.ts +++ b/extension/src/claude-runtime.ts @@ -122,6 +122,7 @@ export async function startClaudeRuntime( // Route watcher events + lifecycle → panel (with orchestrator completion filter) const wiring = wireWatcherToPanel(watcher, { sessionLabelPrefix: 'Claude', + runtime: 'claude', transformEvent: filterOrchestratorCompletion, }) diff --git a/extension/src/codex-runtime.ts b/extension/src/codex-runtime.ts index ff9f24d..b0a83d8 100644 --- a/extension/src/codex-runtime.ts +++ b/extension/src/codex-runtime.ts @@ -22,6 +22,7 @@ export function startCodexRuntime(context: vscode.ExtensionContext): AgentRuntim const wiring = wireWatcherToPanel(watcher, { sessionLabelPrefix: 'Codex', + runtime: 'codex', }) watcher.start() diff --git a/extension/src/codex-session-watcher.ts b/extension/src/codex-session-watcher.ts index bea73c5..b60620f 100644 --- a/extension/src/codex-session-watcher.ts +++ b/extension/src/codex-session-watcher.ts @@ -168,6 +168,7 @@ export class CodexSessionWatcher implements AgentSessionWatcher { return Array.from(this.sessions.values()).map(s => ({ id: s.sessionId, label: s.label, + runtime: 'codex', status: s.sessionCompleted ? 'completed' : 'active', startTime: s.sessionStartTime, lastActivityTime: s.lastActivityTime, diff --git a/extension/src/pi-runtime.ts b/extension/src/pi-runtime.ts index 3dbb0fe..584f171 100644 --- a/extension/src/pi-runtime.ts +++ b/extension/src/pi-runtime.ts @@ -15,7 +15,7 @@ export function startPiRuntime(context: vscode.ExtensionContext): AgentRuntime { const watcher = new PiSessionWatcher(workspace, configured ? { sessionDir: configured } : {}) context.subscriptions.push(watcher) - const wiring = wireWatcherToPanel(watcher, { sessionLabelPrefix: 'Pi' }) + const wiring = wireWatcherToPanel(watcher, { sessionLabelPrefix: 'Pi', runtime: 'pi' }) watcher.start() const rootLabel = watcher.getWatchRoot().replace(os.homedir(), '~') diff --git a/extension/src/pi-session-watcher.ts b/extension/src/pi-session-watcher.ts index 6a00806..1f7e056 100644 --- a/extension/src/pi-session-watcher.ts +++ b/extension/src/pi-session-watcher.ts @@ -145,6 +145,7 @@ export class PiSessionWatcher implements AgentSessionWatcher { return [...this.sessions.values()].filter(session => session.sessionDetected).map(session => ({ id: session.sessionId, label: session.label, + runtime: 'pi', status: session.sessionCompleted ? 'completed' : 'active', startTime: session.sessionStartTime, lastActivityTime: session.lastActivityTime, diff --git a/extension/src/protocol.ts b/extension/src/protocol.ts index ea05743..a2925df 100644 --- a/extension/src/protocol.ts +++ b/extension/src/protocol.ts @@ -34,6 +34,7 @@ export interface SessionInfo { status: 'active' | 'completed' startTime: number lastActivityTime: number + runtime?: 'claude' | 'codex' | 'pi' } // ─── Extension → Webview Messages ──────────────────────────────────────────── diff --git a/extension/src/session-runtime.ts b/extension/src/session-runtime.ts index 9126ed9..8f07a1c 100644 --- a/extension/src/session-runtime.ts +++ b/extension/src/session-runtime.ts @@ -52,6 +52,7 @@ export interface AgentRuntime { export interface WatchPanelWiringOptions { /** Human-readable prefix for the session badge (e.g. "Claude", "Codex"). */ sessionLabelPrefix: string + runtime: AgentRuntimeMode /** Optional event transform — return null to suppress an event. */ transformEvent?: (event: AgentEvent) => AgentEvent | null } @@ -102,6 +103,7 @@ export function wireWatcherToPanel( session: session ?? { id: lifecycle.sessionId, label: lifecycle.label, + runtime: options.runtime, status: 'active', startTime: Date.now(), lastActivityTime: Date.now(), diff --git a/extension/src/session-watcher.ts b/extension/src/session-watcher.ts index 354b814..b8eea01 100644 --- a/extension/src/session-watcher.ts +++ b/extension/src/session-watcher.ts @@ -95,6 +95,7 @@ export class SessionWatcher implements AgentSessionWatcher { return Array.from(this.sessions.values()).map(s => ({ id: s.sessionId, label: s.label, + runtime: 'claude', status: s.sessionCompleted ? 'completed' : 'active', startTime: s.sessionStartTime, lastActivityTime: s.lastActivityTime, diff --git a/extension/test/pi-session-watcher.test.ts b/extension/test/pi-session-watcher.test.ts index 5734ff1..fd9c5b0 100644 --- a/extension/test/pi-session-watcher.test.ts +++ b/extension/test/pi-session-watcher.test.ts @@ -67,6 +67,7 @@ describe('PiSessionWatcher', () => { const { watcher, events } = watch(workspace, { agentDir: path.join(root, 'agent'), env: {}, homeDir: root }) assert.deepEqual(watcher.getActiveSessions().map(session => session.id).sort(), ['descendant-session', 'main-session']) + assert.ok(watcher.getActiveSessions().every(session => session.runtime === 'pi')) const childSpawn = events.find(event => event.type === 'agent_spawn' && event.payload.parent === 'orchestrator') assert.equal(childSpawn?.payload.name, 'delegate-4c85f1e1-1') assert.equal(childSpawn?.payload.runtime, 'pi') diff --git a/scripts/relay.ts b/scripts/relay.ts index b65818c..b226207 100644 --- a/scripts/relay.ts +++ b/scripts/relay.ts @@ -102,14 +102,19 @@ function broadcastEvent(event: AgentEvent) { broadcast(JSON.stringify({ type: 'agent-event', event })) } -function broadcastSessionLifecycle(type: 'started' | 'ended' | 'updated', sessionId: string, label: string) { +function broadcastSessionLifecycle( + type: 'started' | 'ended' | 'updated', + sessionId: string, + label: string, + runtime: SessionInfo['runtime'] = 'claude', +) { if (type === 'started') { const watched = sessions.get(sessionId) const now = Date.now() broadcast(JSON.stringify({ type: 'session-started', session: { - id: sessionId, label, status: 'active', + id: sessionId, label, status: 'active', runtime, startTime: watched?.sessionStartTime ?? now, lastActivityTime: watched?.lastActivityTime ?? now, } as SessionInfo, @@ -453,7 +458,7 @@ export async function createRelay(options: RelayOptions): Promise { if (lifecycle.type === 'activity' && lifecycle.lastActivityTime !== undefined) { broadcastSessionActivity(lifecycle.sessionId, lifecycle.lastActivityTime) } else if (lifecycle.type !== 'activity') { - broadcastSessionLifecycle(lifecycle.type, lifecycle.sessionId, lifecycle.label) + broadcastSessionLifecycle(lifecycle.type, lifecycle.sessionId, lifecycle.label, 'codex') } }) codexWatcher.start() @@ -471,7 +476,7 @@ export async function createRelay(options: RelayOptions): Promise { if (lifecycle.type === 'activity' && lifecycle.lastActivityTime !== undefined) { broadcastSessionActivity(lifecycle.sessionId, lifecycle.lastActivityTime) } else if (lifecycle.type !== 'activity') { - broadcastSessionLifecycle(lifecycle.type, lifecycle.sessionId, lifecycle.label) + broadcastSessionLifecycle(lifecycle.type, lifecycle.sessionId, lifecycle.label, 'pi') } }) piWatcher.start() @@ -523,12 +528,12 @@ export async function createRelay(options: RelayOptions): Promise { log(`[sse] Client disconnected (${sseClients.size} total)`) }) - // Send current session list (Claude + Codex) + // Send current session list (Claude + Codex + Pi) const sessionList: SessionInfo[] = [] for (const session of sessions.values()) { if (!session.sessionDetected) continue sessionList.push({ - id: session.sessionId, label: session.label, + id: session.sessionId, label: session.label, runtime: 'claude', status: session.sessionCompleted ? 'completed' : 'active', startTime: session.sessionStartTime, lastActivityTime: session.lastActivityTime, }) diff --git a/scripts/session-command-palette.test.ts b/scripts/session-command-palette.test.ts index ee2dc71..688928c 100644 --- a/scripts/session-command-palette.test.ts +++ b/scripts/session-command-palette.test.ts @@ -4,9 +4,9 @@ import type { SessionInfo } from '../web/lib/bridge-types.ts' import { filterAndSortSessions, formatRelativeTime } from '../web/lib/session-search.ts' const sessions: SessionInfo[] = [ - { id: 'older-active', label: 'Older active', status: 'active', startTime: 1, lastActivityTime: 20 }, + { id: 'older-active', label: 'Older active', status: 'active', startTime: 1, lastActivityTime: 20, runtime: 'claude' }, { id: 'completed', label: 'Completed task', status: 'completed', startTime: 2, lastActivityTime: 40 }, - { id: 'newer-active', label: 'Newer active', status: 'active', startTime: 3, lastActivityTime: 30 }, + { id: 'newer-active', label: 'Newer active', status: 'active', startTime: 3, lastActivityTime: 30, runtime: 'pi' }, ] test('pins active sessions before completed sessions and sorts each group by activity', () => { @@ -16,9 +16,11 @@ test('pins active sessions before completed sessions and sorts each group by act ) }) -test('searches session labels, IDs, and status', () => { +test('searches session labels, IDs, status, and available runtime metadata', () => { assert.deepEqual(filterAndSortSessions(sessions, 'completed').map(session => session.id), ['completed']) assert.deepEqual(filterAndSortSessions(sessions, 'newer-active').map(session => session.id), ['newer-active']) + assert.deepEqual(filterAndSortSessions(sessions, 'claude').map(session => session.id), ['older-active']) + assert.deepEqual(filterAndSortSessions(sessions, 'pi').map(session => session.id), ['newer-active']) }) test('formats relative activity times', () => { diff --git a/web/components/agent-visualizer/session-command-palette.tsx b/web/components/agent-visualizer/session-command-palette.tsx index f42ea19..65e3b21 100644 --- a/web/components/agent-visualizer/session-command-palette.tsx +++ b/web/components/agent-visualizer/session-command-palette.tsx @@ -13,6 +13,12 @@ interface SessionCommandPaletteProps { onCloseSession: (id: string) => void } +const RUNTIME_BADGES = { + claude: { label: 'CLAUDE', color: COLORS.contextReasoning }, + codex: { label: 'CODEX', color: COLORS.complete }, + pi: { label: 'PI', color: COLORS.tool_calling }, +} as const + export function SessionCommandPalette({ sessions, selectedSessionId, @@ -160,6 +166,7 @@ export function SessionCommandPalette({ const isSelected = session.id === selectedSessionId const hasActivity = sessionsWithActivity.has(session.id) const isActive = session.status === 'active' + const runtimeBadge = session.runtime ? RUNTIME_BADGES[session.runtime] : null return (
  • + {runtimeBadge && ( + + {runtimeBadge.label} + + )} {session.label} diff --git a/web/lib/bridge-types.ts b/web/lib/bridge-types.ts index 60f7f26..1e5868c 100644 --- a/web/lib/bridge-types.ts +++ b/web/lib/bridge-types.ts @@ -19,6 +19,7 @@ export interface SessionInfo { status: 'active' | 'completed' startTime: number lastActivityTime: number + runtime?: 'claude' | 'codex' | 'pi' } export type ConnectionStatus = 'connected' | 'disconnected' | 'watching' diff --git a/web/lib/session-search.ts b/web/lib/session-search.ts index 0c35fa2..7295540 100644 --- a/web/lib/session-search.ts +++ b/web/lib/session-search.ts @@ -4,7 +4,7 @@ export function filterAndSortSessions(sessions: SessionInfo[], query: string): S const normalizedQuery = query.trim().toLowerCase() return sessions - .filter(session => !normalizedQuery || [session.label, session.id, session.status] + .filter(session => !normalizedQuery || [session.label, session.id, session.status, session.runtime ?? ''] .some(value => value.toLowerCase().includes(normalizedQuery))) .sort((a, b) => { if (a.status !== b.status) return a.status === 'active' ? -1 : 1