Move the plugin onto the DSH 0.1.5-alpha.1 baseline (peer floor ^0.1.5-alpha.1, CI pinned to the alpha dist-tag) and fix the two host contract changes that alpha line introduced. Baseline: - all @deepseek-ai/dsh-* peer/dev deps -> 0.1.5-alpha.1; optional peer @deepseek-ai/dsh-client-ui-sidebar-right (declared, unused until the native-surface phase); devDep @deepseek-ai/dsh-agent-loop (inbox fold) - dsh.plugin.json engines >=0.1.5-alpha.1; version 0.19.0-alpha.0; SIDEBAR_SERVICE_VERSION follows Live assistant stream (assistant/chunk is gone): - 0.1.5 publishes in-flight model deltas as process-local agent/assistant-stream frames instead of durable assistant/chunk events and embeds the settled stream in assistant/message / assistant/attempt. src/assistant-live.ts folds the frames into a bounded per-session buffer; sidechat.events serves the current attempt as `live` rows (replaced every poll, never a delta) and the transcript renders them; the inherited in-progress snapshot reads message content / attempt stream / live frames. Cold session reads (persistence.inspect is gone): - 0.1.5 reads a stored session through an explicit read handle (open(id,'read') -> handle.read() -> close()). src/session-store.ts owns that protocol; every cold read (sidechat transcript, cold-resume preset, session.cwd, changes replay) goes through it. Dead code and docs: - drop the remote.session.openWorkspacePath interception: 0.1.5's chat opens files through ctx.sidebarRight.openResource instead, so the wrapper had no caller and would hijack the host's "open in app" gesture; the interceptOpenPath pref, its settings row and its locale keys go with it - AGENTS.md §3 + external-plugin-guide.md describe the new baseline - vitest setup bridges Node's localStorage accessor to jsdom's store (the pre-existing 26 failures on Node >= 22) - desktop-layout e2e seeds through host RPC and types into the Lexical composer (0.1.5 replaced the textarea) instead of driving the directory picker Verified: pnpm typecheck / lint / build / test (124 files, 1327 tests) / check:consumer-types, plus pnpm test:mount against a real @deepseek-ai/dsh@0.1.5-alpha.1 host (18/18).
94 lines
4.1 KiB
TypeScript
94 lines
4.1 KiB
TypeScript
/**
|
|
* Pure derivation of the compact LIVE line shown on a running subagent card:
|
|
* the last text output and the last tool call of the child's session events.
|
|
* The host batch route feeds raw session events into this parser; the client
|
|
* only receives the already-folded `LastActivity` map. Renders nothing
|
|
* itself — the SubagentView component turns this into the card's status
|
|
* lines. Kept framework-free so the parser is unit-testable in the node
|
|
* environment.
|
|
*/
|
|
import type { SidebarSessionEvent } from './context-types.ts'
|
|
|
|
/**
|
|
* Extract the concatenated plain text of a content-block list (the durable
|
|
* `ContentBlock[]` shape, structurally: blocks with `type: 'text'` carry
|
|
* `text`; anything else — tool_use, image, … — contributes nothing).
|
|
* @param content - the raw `content` field of a message event.
|
|
* @returns the joined text, or undefined when the message carries no text.
|
|
*/
|
|
export function contentText(content: unknown): string | undefined {
|
|
if (!Array.isArray(content)) return undefined
|
|
const parts: string[] = []
|
|
for (const block of content) {
|
|
if (block === null || typeof block !== 'object') continue
|
|
const candidate = block as { type?: unknown; text?: unknown }
|
|
if (candidate.type === 'text' && typeof candidate.text === 'string') {
|
|
parts.push(candidate.text)
|
|
}
|
|
}
|
|
return parts.length > 0 ? parts.join('\n') : undefined
|
|
}
|
|
|
|
/** The live status of one subagent card (both fields optional). */
|
|
export interface LastActivity {
|
|
/** The latest assembled assistant text output in the tail. */
|
|
text?: string
|
|
/** The latest tool call in the tail. */
|
|
tool?: { name: string; args: string }
|
|
}
|
|
|
|
/**
|
|
* Fold a session event log into the last text output + last tool call (each
|
|
* is the LAST occurrence in event order). Lifecycle events and raw stream
|
|
* rows are ignored — the card shows what the subagent is doing right now,
|
|
* not its plumbing. (DSH 0.1.5 publishes the raw deltas as process-local
|
|
* frames instead of log events, so only the assembled `assistant/message`
|
|
* reaches this scan.) The scan runs BACKWARD from the newest
|
|
* event and stops once both fields are found, so a long history costs only
|
|
* the recent tail in the common case.
|
|
* @param events - the session's append-only event log (oldest → newest).
|
|
* @param maxMessages - optional message-boundary window: only the tail's
|
|
* last `maxMessages` surface messages (`user/message`, `assistant/message`)
|
|
* and the events between them are considered, mirroring the old
|
|
* `subagents.history({ maxMessages })` window. Stale activity older than
|
|
* the window is never surfaced, and a long log is never scanned in full.
|
|
* @returns the last text and/or tool call; an empty object when the log has neither.
|
|
*/
|
|
export function lastActivity(
|
|
events: readonly SidebarSessionEvent[],
|
|
maxMessages = Infinity,
|
|
): LastActivity {
|
|
let text: string | undefined
|
|
let tool: { name: string; args: string } | undefined
|
|
let messagesSeen = 0
|
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
if (text !== undefined && tool !== undefined) break
|
|
const event = events[index]
|
|
if (event === undefined) continue
|
|
const { type, data } = event
|
|
if (type === 'user/message' || type === 'assistant/message') {
|
|
messagesSeen += 1
|
|
if (messagesSeen > maxMessages) break
|
|
} else if (messagesSeen >= maxMessages) {
|
|
// The window already holds its `maxMessages` messages: anything older
|
|
// than the oldest in-window message sits outside the recent window.
|
|
continue
|
|
}
|
|
if (text === undefined && type === 'assistant/message') {
|
|
const message = data.message as { content?: unknown } | undefined
|
|
const extracted = contentText(message?.content)
|
|
if (extracted !== undefined) text = extracted
|
|
} else if (tool === undefined && type === 'tool/call') {
|
|
tool = {
|
|
name: typeof data.name === 'string' ? data.name : 'tool',
|
|
args: typeof data.arguments === 'string' ? data.arguments : '',
|
|
}
|
|
}
|
|
}
|
|
if (text === undefined && tool === undefined) return {}
|
|
return {
|
|
...(text === undefined ? {} : { text }),
|
|
...(tool === undefined ? {} : { tool }),
|
|
}
|
|
}
|