mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(pi): keep archived sessions visible (#1297)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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' } }),
|
||||
|
||||
Reference in New Issue
Block a user