feat(dsh): adapt to DSH 0.1.5-alpha.1

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).
This commit is contained in:
Menghuan1918
2026-09-09 11:15:30 +08:00
parent d88dcfc3a5
commit ba0114749f
59 changed files with 1163 additions and 1011 deletions
+33 -12
View File
@@ -7,6 +7,7 @@
*/
import { describe, expect, it } from 'vitest'
import type { SidebarHistoryEntry, SidebarSessionEvent, SidebarSessionSummary } from '../src/context-types.ts'
import type { AssistantLiveChunk } from '../src/assistant-live.ts'
import {
boundaryDelivered,
buildOpenTurnSnapshot,
@@ -35,6 +36,24 @@ function ev(type: string, seq: number, data: Record<string, unknown> = {}): Side
return event
}
/** One live delta as the 0.1.5 stream buffer produces it: `index` is the
* dense frame position, the chunk carries its own block index. */
function live(
turn: number,
step: number,
text: string,
over: { kind?: 'text' | 'reasoning'; index?: number; block?: number } = {},
): AssistantLiveChunk {
return {
attemptId: 'a1',
turn,
step,
index: over.index ?? 0,
time: 0,
chunk: { type: over.kind === 'reasoning' ? 'reasoning-delta' : 'text-delta', index: over.block ?? 0, text },
}
}
/** A completed user→assistant turn block (turn T, step 1, optional tools). */
function completedTurn(seq: number, turn: number, over: {
text?: string
@@ -136,7 +155,6 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 6, { content: [{ type: 'text', text: 'next?' }], source: { kind: 'user' } }),
ev('turn/start', 7, { turn: 2 }),
ev('step/start', 8, { turn: 2, step: 1 }),
ev('assistant/chunk', 9, { turn: 2, step: 1, chunk: { type: 'text-delta', index: 0, text: 'partial answer' } }),
]
const { seed, snapshot } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -145,10 +163,12 @@ describe('buildSidechatInheritance', () => {
const turnEnd = seed.at(-1)!
expect(stepEnd).toMatchObject({ type: 'step/end', data: { turn: 2, step: 1 } })
expect(turnEnd).toMatchObject({ type: 'turn/end', data: { turn: 2, reason: { kind: 'interrupted' } } })
// The pending user message and the partial chunk survive as REAL events.
// The pending user message survives as a REAL event. The in-flight
// step's deltas are process-local on 0.1.5, so they are NOT seed events —
// the snapshot fallback is the only path that carries them.
expect(seed.some(event => event.type === 'user/message'
&& (event.data.content as Array<{ text: string }>)[0]?.text === 'next?')).toBe(true)
expect(seed.some(event => event.type === 'assistant/chunk')).toBe(true)
expect(seed.every(event => event.type !== 'assistant/live-chunk')).toBe(true)
expect(snapshot).toBeNull()
})
@@ -166,7 +186,7 @@ describe('buildSidechatInheritance', () => {
content: [{ type: 'tool-result', toolCallId: 'c1', content: [{ type: 'text', text: 'hit' }] }],
},
}),
ev('assistant/chunk', 5, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'found ' } }),
ev('assistant/message', 5, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'found it' }] }, stream: [] }),
]
const { seed, snapshot } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -180,11 +200,12 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 0, { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/chunk', 3, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'running ' } }),
ev('tool/call', 4, { turn: 1, step: 1, callId: 'c1', name: 'bash', arguments: '{"cmd":"sleep 9"}' }),
]
expect(hasDanglingToolCall(events, 1)).toBe(true)
const { seed, snapshot } = buildSidechatInheritance(events)
// The in-flight text is process-local on 0.1.5: it reaches the snapshot
// through the live buffer, not through the log.
const { seed, snapshot } = buildSidechatInheritance(events, [live(1, 1, 'running ')])
expectContiguous(seed)
// The seed stops BEFORE the open turn; the pending user message stays.
expect(seed.at(-1)?.type).toBe('user/message')
@@ -200,10 +221,9 @@ describe('buildSidechatInheritance', () => {
ev('user/message', 0, { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }),
ev('turn/start', 1, { turn: 1 }),
ev('step/start', 2, { turn: 1, step: 1 }),
ev('assistant/message', 3, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'first' }] } }),
ev('assistant/message', 3, { turn: 1, step: 1, message: { content: [{ type: 'text', text: 'first' }] }, stream: [] }),
ev('step/end', 4, { turn: 1, step: 1 }),
ev('step/start', 5, { turn: 1, step: 2 }),
ev('assistant/chunk', 6, { turn: 1, step: 2, chunk: { type: 'text-delta', index: 0, text: 'second' } }),
]
const { seed } = buildSidechatInheritance(events)
expectContiguous(seed)
@@ -222,9 +242,6 @@ describe('buildOpenTurnSnapshot', () => {
const events = [
ev('turn/start', 0, { turn: 1 }),
ev('step/start', 1, { turn: 1, step: 1 }),
ev('assistant/chunk', 2, { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'think' } }),
ev('assistant/chunk', 3, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: '```js\ncode' } }),
ev('assistant/chunk', 4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: '\n```' } }),
ev('tool/call', 5, { turn: 1, step: 1, callId: 'c1', name: 'read', arguments: '{"path":"a.txt"}' }),
ev('tool/result', 6, {
turn: 1,
@@ -236,7 +253,11 @@ describe('buildOpenTurnSnapshot', () => {
}),
ev('tool/call', 7, { turn: 1, step: 1, callId: 'c2', name: 'bash', arguments: '{"cmd":"long"}' }),
]
const snapshot = buildOpenTurnSnapshot(events)
const snapshot = buildOpenTurnSnapshot(events, [
live(1, 1, 'think', { kind: 'reasoning', index: 0, block: 0 }),
live(1, 1, '```js\ncode', { index: 1, block: 1 }),
live(1, 1, '\n```', { index: 2, block: 1 }),
])
expect(snapshot).not.toBeNull()
expect(snapshot).toContain('```js\ncode\n```')
expect(snapshot).toContain('Reasoning so far')