Conflict resolutions: - `session.list`: master's projection columns fold into the PR's cancellable, batched `listVisibleSessionSummaries`, which `session.search` shares as its visibility baseline; master's goal helpers stay beside it. - Client sessions face: master narrowed `ctx.sessions` to `ISessions`, so the search verb and its protocol-constant bound are declared there and the test-runtime double implements them (recorded, empty page unless a scenario stubs hits). - `WorkspaceBrowser`: master's per-row Rename wiring rides the PR's search results view; the tree keeps the PR's query-free derivations. - `dsh web` bin: the PR's shutdown-handlers-before-readiness order with master's boot-time LAN address snapshot. - `session-query-sqlite`: master's `SCHEMA_VERSION` 7 stands; the PR's bump carried no schema change. - Specs: master wraps assistant/steering message payloads and requires an `application/json` carrier request, so the search fixtures and tests follow. - Web aria goldens keep master's recording plus the PR's search placeholder; the navigation-panes inventory keeps master's terminal-card golden next to the PR's search-results golden.
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
/** First-party semantic text extraction for session-query consumers. */
|
|
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
|
/**
|
|
* Extract searchable semantic text from one first-party session event.
|
|
*
|
|
* Structural boundaries, raw stream chunks, request envelopes, and unknown
|
|
* declaration-merged events contribute no text.
|
|
* @param event - event to inspect.
|
|
* @returns newline-joined semantic text, or an empty string when non-searchable.
|
|
*/
|
|
export function extractSessionEventText(event: SessionEvent): string {
|
|
switch (event.type) {
|
|
case 'user/message':
|
|
return contentText(event.data.content)
|
|
case 'assistant/message':
|
|
case 'steering/message':
|
|
return contentText(event.data.message.content)
|
|
case 'tool/call':
|
|
return joinText([event.data.name, event.data.arguments])
|
|
case 'tool/result':
|
|
return joinText([
|
|
contentText(event.data.message.content),
|
|
event.data.error?.name ?? '',
|
|
event.data.error?.code ?? '',
|
|
])
|
|
case 'todo/write':
|
|
return joinText(event.data.todos.flatMap(todo => [todo.status, todo.content]))
|
|
case 'turn/end':
|
|
return turnEndText(event.data.reason)
|
|
case 'turn/start':
|
|
case 'step/start':
|
|
case 'step/end':
|
|
case 'assistant/chunk':
|
|
case 'request/header':
|
|
return ''
|
|
// SessionEventMap is merge-extensible. Unknown events remain
|
|
// non-searchable until a concrete first-party consumer defines semantics.
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function turnEndText(reason: SessionEvent<'turn/end'>['data']['reason']): string {
|
|
switch (reason.kind) {
|
|
case 'error':
|
|
return 'failure' in reason
|
|
? joinText(['error', reason.failure.message, reason.failure.code])
|
|
: joinText(['error', reason.message, reason.code ?? ''])
|
|
case 'aborted':
|
|
return 'aborted'
|
|
case 'disposed':
|
|
case 'max-tokens':
|
|
case 'interrupted':
|
|
return reason.kind
|
|
case 'completed':
|
|
return ''
|
|
// TurnEndReasonMap is merge-extensible. Unknown outcomes stay out until
|
|
// their owner defines which detail is semantic rather than structural.
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
type SessionContentBlock = SessionEvent<'user/message'>['data']['content'][number]
|
|
|
|
function contentText(content: readonly SessionContentBlock[]): string {
|
|
return joinText(content.flatMap(blockText))
|
|
}
|
|
|
|
function blockText(block: SessionContentBlock): string[] {
|
|
switch (block.type) {
|
|
case 'text':
|
|
return [block.text]
|
|
case 'reasoning':
|
|
return []
|
|
case 'tool-call':
|
|
return [block.name, block.arguments]
|
|
case 'tool-result':
|
|
return block.content.flatMap(blockText)
|
|
// ContentBlockMap is merge-extensible. Unknown blocks do not become
|
|
// searchable merely because their payload happens to contain strings.
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
function joinText(parts: readonly string[]): string {
|
|
return parts.map(part => part.trim()).filter(Boolean).join('\n')
|
|
}
|