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
+26 -10
View File
@@ -37,17 +37,21 @@ import type {
import {
boundaryDelivered,
buildSidechatInheritance,
liveEventsOf,
resolvePresetId,
SIDE_BOUNDARY_PROMPT,
SIDE_INJECTION_PLUGIN,
SIDE_NEW_THREAD_TITLE,
sideLabel,
type SeedEvent,
type SidechatLiveEvent,
type SidechatLogEvent,
type SidechatThreadInfo,
threadOwnLogEvents,
} from './sidechat-core.ts'
import type { AssistantLiveBuffer } from './assistant-live.ts'
import { requireString, SidebarError } from './wire.ts'
import { readPersistedSession } from './session-store.ts'
/** The six Side Chat routes of the sidebar API (wire method names). */
export interface SidechatRoutes {
@@ -66,8 +70,10 @@ export interface SidechatRoutes {
'sidechat.info'(payload: unknown): Promise<SidechatThreadInfo>
/** The thread's OWN transcript events, seed-cut host-side (the inherited
* parent log never crosses the wire); `afterSeq` narrows the response to
* the delta beyond it (poll tail). */
'sidechat.events'(payload: unknown): Promise<{ events: SidechatLogEvent[] }>
* the delta beyond it (poll tail). `live` carries the thread's in-flight
* model deltas, which DSH 0.1.5 publishes outside the session log — it is
* the CURRENT attempt's rows on every poll, never a delta. */
'sidechat.events'(payload: unknown): Promise<{ events: SidechatLogEvent[]; live: SidechatLiveEvent[] }>
}
/** Timeout guarding the create call (the registry detaches it before the
@@ -117,8 +123,8 @@ async function composePersistedSetup(
if (persistence === undefined) {
return () => Promise.resolve()
}
const inspected = await persistence.inspect(childId)
const presetId = resolvePresetId(inspected.meta, inspected.events)
const inspected = await readPersistedSession(persistence, childId)
const presetId = resolvePresetId(inspected.header, inspected.events)
const presets = ctx.get('agentPresets') as SidebarAgentPresetsService | undefined
if (presets === undefined || presetId === undefined) {
return () => Promise.resolve()
@@ -186,7 +192,7 @@ async function threadLogEvents(ctx: Context, childId: string): Promise<readonly
throw new SidebarError('sidechat-error', 'the session persistence service is unavailable', 503)
}
try {
const inspected = await persistence.inspect(childId)
const inspected = await readPersistedSession(persistence, childId)
return inspected.events as unknown as readonly SidechatLogEvent[]
} catch (error: unknown) {
throw new SidebarError(
@@ -199,8 +205,11 @@ async function threadLogEvents(ctx: Context, childId: string): Promise<readonly
/** Build the Side Chat routes (all optional services degrade to a wire
* error the tab surfaces inline). The record keys are the FULL wire method
* names the /sidebar/api dispatcher looks up (`api[method]`). */
export function buildSidechatApi(ctx: Context): SidechatRoutes {
* names the /sidebar/api dispatcher looks up (`api[method]`).
* @param ctx - host plugin context.
* @param live - the live assistant stream buffer; absent only in tests that
* never exercise streaming (then every `live` response is empty). */
export function buildSidechatApi(ctx: Context, live?: AssistantLiveBuffer): SidechatRoutes {
return {
'sidechat.start': async (payload: unknown) => {
const sessionId = requireString(payload, 'sessionId')
@@ -213,6 +222,7 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const parentSession = parent.session
const inheritance = buildSidechatInheritance(
parentSession.snapshotEvents() as unknown as readonly SidechatLogEvent[],
live?.chunksFor(sessionId) ?? [],
)
const { agentPreset, setup } = await composeChildSetup(
ctx,
@@ -388,8 +398,8 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined
if (persistence !== undefined) {
try {
const inspected = await persistence.inspect(childId)
const preset = resolvePresetId(inspected.meta, inspected.events)
const inspected = await readPersistedSession(persistence, childId)
const preset = resolvePresetId(inspected.header, inspected.events)
return { live: false, ...(preset === undefined ? {} : { preset }) }
} catch {
// Unknown/gone session: report a bare cold info.
@@ -408,7 +418,13 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
const events = await threadLogEvents(ctx, childId)
const own = threadOwnLogEvents(events)
const fresh = rawAfter === undefined ? own : own.filter(event => event.seq > rawAfter)
return { events: fresh.length > EVENTS_CAP ? fresh.slice(fresh.length - EVENTS_CAP) : fresh }
const tailSeq = own.at(-1)?.seq ?? -1
return {
events: fresh.length > EVENTS_CAP ? fresh.slice(fresh.length - EVENTS_CAP) : fresh,
// The live rows ride every poll: they are process-local, so there is
// no durable seq to page them by. An idle thread has none.
live: liveEventsOf(live?.chunksFor(childId) ?? [], tailSeq),
}
},
}
}