Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extension/src/claude-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down
1 change: 1 addition & 0 deletions extension/src/codex-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function startCodexRuntime(context: vscode.ExtensionContext): AgentRuntim

const wiring = wireWatcherToPanel(watcher, {
sessionLabelPrefix: 'Codex',
runtime: 'codex',
})

watcher.start()
Expand Down
1 change: 1 addition & 0 deletions extension/src/codex-session-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion extension/src/pi-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(), '~')
Expand Down
1 change: 1 addition & 0 deletions extension/src/pi-session-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions extension/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface SessionInfo {
status: 'active' | 'completed'
startTime: number
lastActivityTime: number
runtime?: 'claude' | 'codex' | 'pi'
}

// ─── Extension → Webview Messages ────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions extension/src/session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions extension/src/session-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions extension/test/pi-session-watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
17 changes: 11 additions & 6 deletions scripts/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -453,7 +458,7 @@ export async function createRelay(options: RelayOptions): Promise<Relay> {
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()
Expand All @@ -471,7 +476,7 @@ export async function createRelay(options: RelayOptions): Promise<Relay> {
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()
Expand Down Expand Up @@ -523,12 +528,12 @@ export async function createRelay(options: RelayOptions): Promise<Relay> {
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,
})
Expand Down
8 changes: 5 additions & 3 deletions scripts/session-command-palette.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
15 changes: 15 additions & 0 deletions web/components/agent-visualizer/session-command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<li
key={session.id}
Expand Down Expand Up @@ -189,6 +196,14 @@ export function SessionCommandPalette({
boxShadow: hasActivity && !isSelected ? `0 0 4px ${COLORS.complete}` : 'none',
}}
/>
{runtimeBadge && (
<span
className="flex-shrink-0 rounded border px-1 py-0.5 text-[8px] font-semibold tracking-wide"
style={{ color: runtimeBadge.color, borderColor: `${runtimeBadge.color}60`, background: COLORS.holoBg05 }}
>
{runtimeBadge.label}
</span>
)}
<span className="truncate" style={{ color: isSelected ? COLORS.holoBright : COLORS.textPrimary }}>
{session.label}
</span>
Expand Down
1 change: 1 addition & 0 deletions web/lib/bridge-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface SessionInfo {
status: 'active' | 'completed'
startTime: number
lastActivityTime: number
runtime?: 'claude' | 'codex' | 'pi'
}

export type ConnectionStatus = 'connected' | 'disconnected' | 'watching'
2 changes: 1 addition & 1 deletion web/lib/session-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading