fix(pi): keep archived sessions visible (#1297)

This commit is contained in:
KorenKrita
2026-08-02 10:00:15 +08:00
committed by GitHub
parent f213bc348c
commit 1ca7af44d2
5 changed files with 131 additions and 11 deletions
+2 -1
View File
@@ -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(
+2 -1
View File
@@ -60,7 +60,8 @@ const SIMPLE_RESUME_TOKENS = [
'opencodeSessionId',
'grokSessionId',
'cursorSessionId',
'kimiSessionId'
'kimiSessionId',
'piSessionId'
] as const
function isPlainObject(value: unknown): value is Record<string, unknown> {
+57
View File
@@ -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,
+36 -9
View File
@@ -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<AgentFlavor, keyof Metadata>
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
+34
View File
@@ -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' } }),