Show first user message in resume picker

This commit is contained in:
weishu
2026-05-20 20:31:40 +08:00
parent 2ef90f84fb
commit 856af6d8b2
9 changed files with 137 additions and 6 deletions
+51 -1
View File
@@ -9,6 +9,7 @@
import type { LocalResumeTarget, ResumableSession } from '@hapi/protocol'
import type { AgentFlavor, CodexCollaborationMode, DecryptedMessage, PermissionMode, Session, SyncEvent } from '@hapi/protocol/types'
import { unwrapRoleWrappedRecordEnvelope } from '@hapi/protocol/messages'
import type { Server } from 'socket.io'
import type { Store, CancelQueuedMessageResult } from '../store'
import type { RpcRegistry } from '../socket/rpcRegistry'
@@ -61,6 +62,42 @@ export type LocalHandoffResult =
| { type: 'success' }
| { type: 'error'; message: string; code: 'session_not_found' | 'access_denied' | 'already_local' | 'handoff_failed' }
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null
}
function normalizeUserMessageText(value: string): string | undefined {
const text = value.trim().replace(/\s+/g, ' ')
return text.length > 0 ? text : undefined
}
function extractUserMessageText(content: unknown): string | undefined {
if (typeof content === 'string') {
return normalizeUserMessageText(content)
}
if (Array.isArray(content)) {
const parts = content
.map((block) => {
const record = asRecord(block)
return record?.type === 'text' && typeof record.text === 'string'
? record.text
: null
})
.filter((text): text is string => text !== null)
return normalizeUserMessageText(parts.join(' '))
}
const record = asRecord(content)
if (record?.type === 'text' && typeof record.text === 'string') {
return normalizeUserMessageText(record.text)
}
return undefined
}
export class SyncEngine {
private readonly eventPublisher: EventPublisher
private readonly sessionCache: SessionCache
@@ -521,13 +558,26 @@ export class SyncEngine {
collaborationMode: target.collaborationMode,
updatedAt: session?.updatedAt ?? 0,
name: session?.metadata?.name,
summary: session?.metadata?.summary?.text
summary: session?.metadata?.summary?.text,
firstUserMessage: this.resolveFirstUserMessage(target.sessionId)
}
})
.filter((session) => !opts?.machineId || session.machineId === opts.machineId)
.sort((a, b) => b.updatedAt - a.updatedAt)
}
private resolveFirstUserMessage(sessionId: string): string | undefined {
for (const message of this.store.messages.getFirstMessages(sessionId, 50)) {
const roleWrapped = unwrapRoleWrappedRecordEnvelope(message.content)
if (roleWrapped?.role !== 'user') continue
const text = extractUserMessageText(roleWrapped.content)
if (text) return text
}
return undefined
}
async resumeSession(sessionId: string, namespace: string, opts?: { permissionMode?: PermissionMode }): Promise<ResumeSessionResult> {
const access = this.sessionCache.resolveSessionAccess(sessionId, namespace)
if (!access.ok) {