From 1ca7af44d21345e81a3262d2f36876465f24d656 Mon Sep 17 00:00:00 2001 From: KorenKrita Date: Sun, 2 Aug 2026 10:00:15 +0800 Subject: [PATCH] fix(pi): keep archived sessions visible (#1297) --- hub/src/store/sessions.test.ts | 3 +- hub/src/store/sessions.ts | 3 +- shared/src/sessionSummary.test.ts | 57 ++++++++++++++++++++++++++ shared/src/sessionSummary.ts | 45 ++++++++++++++++---- web/src/components/SessionList.test.ts | 34 +++++++++++++++ 5 files changed, 131 insertions(+), 11 deletions(-) diff --git a/hub/src/store/sessions.test.ts b/hub/src/store/sessions.test.ts index fb7f983c..5fea3be8 100644 --- a/hub/src/store/sessions.test.ts +++ b/hub/src/store/sessions.test.ts @@ -174,7 +174,8 @@ describe('updateSessionMetadata: protocol resume token preservation', () => { ['opencodeSessionId', 'opencode-thread-x'], ['grokSessionId', 'grok-thread-x'], ['cursorSessionId', 'cursor-thread-x'], - ['kimiSessionId', 'kimi-thread-x'] + ['kimiSessionId', 'kimi-thread-x'], + ['piSessionId', 'pi-thread-x'] ])('preserves %s across an archive metadata replacement', (field, value) => { const store = makeStore() const session = store.sessions.getOrCreateSession( diff --git a/hub/src/store/sessions.ts b/hub/src/store/sessions.ts index 9d03e059..9fae4f1f 100644 --- a/hub/src/store/sessions.ts +++ b/hub/src/store/sessions.ts @@ -60,7 +60,8 @@ const SIMPLE_RESUME_TOKENS = [ 'opencodeSessionId', 'grokSessionId', 'cursorSessionId', - 'kimiSessionId' + 'kimiSessionId', + 'piSessionId' ] as const function isPlainObject(value: unknown): value is Record { diff --git a/shared/src/sessionSummary.test.ts b/shared/src/sessionSummary.test.ts index 67bd8319..ac9c0abf 100644 --- a/shared/src/sessionSummary.test.ts +++ b/shared/src/sessionSummary.test.ts @@ -77,6 +77,63 @@ describe('toSessionSummary', () => { expect(summary.metadata?.agentSessionId).toBe('grok-session-1') }) + it('uses the native id matching the current flavor instead of a stale id', () => { + const summary = toSessionSummary(makeSession({ + metadata: { + path: '/proj', + host: 'local', + flavor: 'cursor', + codexSessionId: 'stale-codex-id', + cursorSessionId: 'cursor-session-1' + } + })) + + expect(summary.metadata?.agentSessionId).toBe('cursor-session-1') + }) + + it('does not fall back to a stale cross-agent id for a known flavor', () => { + const summary = toSessionSummary(makeSession({ + metadata: { + path: '/proj', + host: 'local', + flavor: 'pi', + codexSessionId: 'stale-codex-id' + } + })) + + expect(summary.metadata?.agentSessionId).toBeUndefined() + }) + + it('includes piSessionId as the native resume token', () => { + const summary = toSessionSummary(makeSession({ + metadata: { + path: '/proj', + host: 'local', + flavor: 'pi', + piSessionId: 'pi-session-1' + } + })) + + expect(summary.metadata?.agentSessionId).toBe('pi-session-1') + }) + + it.each([ + undefined, + 'custom', + ' PI ' + ])('does not infer a Pi identity for an unknown flavor: %s', (flavor) => { + const summary = toSessionSummary(makeSession({ + metadata: { + path: '/proj', + host: 'local', + flavor, + piSessionId: 'pi-session-1' + } + })) + + expect(summary.metadata?.agentSessionId).toBeUndefined() + }) + it('includes pending request kinds and background task count', () => { const summary = toSessionSummary(makeSession({ backgroundTaskCount: 2, diff --git a/shared/src/sessionSummary.ts b/shared/src/sessionSummary.ts index 5f1b55ec..ba010cb7 100644 --- a/shared/src/sessionSummary.ts +++ b/shared/src/sessionSummary.ts @@ -1,4 +1,6 @@ -import type { Session, WorktreeMetadata } from './schemas' +import type { Metadata, Session, WorktreeMetadata } from './schemas' +import { isKnownFlavor } from './flavors' +import type { AgentFlavor } from './modes' export type PendingRequestKind = 'permission' | 'input' @@ -106,6 +108,38 @@ export function getPendingRequestKinds(session: Session): PendingRequestKind[] { : Array.from(kinds) } +const AGENT_SESSION_ID_FIELD_BY_FLAVOR = { + claude: 'claudeSessionId', + codex: 'codexSessionId', + gemini: 'geminiSessionId', + opencode: 'opencodeSessionId', + grok: 'grokSessionId', + cursor: 'cursorSessionId', + kimi: 'kimiSessionId', + pi: 'piSessionId' +} as const satisfies Record + +function getSummaryAgentSessionId(metadata: Metadata): string | undefined { + const flavor = metadata.flavor + if (isKnownFlavor(flavor)) { + const flavorField = AGENT_SESSION_ID_FIELD_BY_FLAVOR[flavor] + const flavorSessionId = metadata[flavorField] + return typeof flavorSessionId === 'string' && flavorSessionId.trim() + ? flavorSessionId.trim() + : undefined + } + + // Legacy fallback only applies when the stored flavor is missing or unknown. + return metadata.codexSessionId + ?? metadata.claudeSessionId + ?? metadata.geminiSessionId + ?? metadata.opencodeSessionId + ?? metadata.grokSessionId + ?? metadata.cursorSessionId + ?? metadata.kimiSessionId + ?? undefined +} + export function toSessionSummary(session: Session): SessionSummary { const pendingRequestsCount = session.agentState?.requests ? Object.keys(session.agentState.requests).length : 0 @@ -116,14 +150,7 @@ export function toSessionSummary(session: Session): SessionSummary { summary: session.metadata.summary ? { text: session.metadata.summary.text } : undefined, flavor: session.metadata.flavor ?? null, worktree: session.metadata.worktree, - agentSessionId: session.metadata.codexSessionId - ?? session.metadata.claudeSessionId - ?? session.metadata.geminiSessionId - ?? session.metadata.opencodeSessionId - ?? session.metadata.grokSessionId - ?? session.metadata.cursorSessionId - ?? session.metadata.kimiSessionId - ?? undefined, + agentSessionId: getSummaryAgentSessionId(session.metadata), lifecycleState: session.metadata.lifecycleState } : null diff --git a/web/src/components/SessionList.test.ts b/web/src/components/SessionList.test.ts index 54b85372..02c49f09 100644 --- a/web/src/components/SessionList.test.ts +++ b/web/src/components/SessionList.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { toSessionSummary, type Session } from '@hapi/protocol' import type { SessionSummary } from '@/types/api' import { deduplicateSessionsByAgentId, @@ -233,6 +234,39 @@ describe('prepareSidebarSessions', () => { expect(result.map(session => session.id)).toEqual(['real']) }) + it('keeps an archived Pi session with a native session id and no title', () => { + const piSession: Session = { + id: 'archived-pi', + namespace: 'default', + seq: 1, + createdAt: 50, + active: false, + activeAt: 0, + updatedAt: 100, + metadata: { + path: '/work/hapi', + host: 'local', + flavor: 'pi', + piSessionId: 'pi-session-1', + lifecycleState: 'archived' + }, + metadataVersion: 1, + agentState: null, + agentStateVersion: 0, + thinking: false, + thinkingAt: 0, + model: null, + modelReasoningEffort: null, + effort: null, + serviceTier: null + } + + const summary = toSessionSummary(piSession) + + expect(summary.metadata?.agentSessionId).toBe('pi-session-1') + expect(prepareSidebarSessions([summary]).map(session => session.id)).toEqual(['archived-pi']) + }) + it('keeps the selected inactive stub visible', () => { const sessions = [ makeSession({ id: 'stub', metadata: { path: '/work/hapi' } }),