feat: adapt to DSH 0.1.7-alpha.1 and yield the read-only previews

DSH 0.1.7-alpha.1 is 1299 commits past 0.1.6-alpha.2 and rewrites four
contracts this plugin was built on. The peer floor moves to
^0.1.7-alpha.1 (semver's prerelease rule means ^0.1.6-alpha.2 can never
match a 0.1.7 prerelease); 0.1.6-alpha.2 and earlier stay on v0.19.1.
Released as 0.21.0-alpha.1 (dist-tag alpha).

Hard breaks:
- dsh-settings was rewritten: register(ns, schema) is gone, a form is
  addressed by the plugin row's Loader entry id and read from the
  module's exported Config. The preferences are merged into Config and
  marked volatile — which requires DSH's own schemastery, because only
  that build wraps a volatile field in the cosmokit reference the
  loader's volatile commit walks. With the public package a settings
  write reports success and revision 1 while the effective value never
  changes.
- ui-primitives renamed its whole icon family (IconXxx14/16 ->
  IconXxxRegular/Medium): 26 imported symbols, 17 files. The failing
  mode is render-time, not load-time. Five call sites that relied on the
  old 14px default now pass size={14} explicitly.
- Session format v4 rejects source.kind === 'plugin'; the side-chat
  boundary injection now writes plugin:dsh-better-sidebar, the same kind
  DSH's own v3->v4 migration produces, so historical threads survive.
  Tool results moved to role 'tool' with top-level content/isError; all
  four parsers accept both shapes.
- The client session snapshot dropped subagentsByParent/jobsBySession
  and the on-demand subagent-catalog observation API. The Tasks page
  reads projectionsBySession and a new jobs.list route.

Yields and convergence:
- The read-only previews go to DSH's documentpreview (Office->PDF, a
  spreadsheet worker, zoom, per-directory refresh); editor.canOpen
  refuses those 26 extensions. Markdown, HTML (with its sandbox
  switches) and the editable code buffer stay.
- The host browser kind is desktop-profile-only now, so the web e2e
  lanes expect no browser entry and assert its absence.
- Link takeover claims only urlTarget links and passes the rest to the
  host, fixing a regression from the 0.1.6 round: the fallback pointed
  at a 'browser' type the plugin no longer registers, so http links in
  its own markdown did nothing.

Also fixed, found along the way:
- The file tree refreshes live (new directory watch over the plugin's
  own socket), since the plugin owns the `files` kind.
- ctx.sessions.list has no `current` field at either tag; the three
  readers were dead. They now use ctx.sidebarRight.mounted.
- package.json.files was missing lib/client-locale.js.
- Two genuinely dead call sites: sessions.openSubagent/open/
  refreshSubagents do not exist on the current baseline, so the
  topology click, the jump-back and the retry were all no-ops.
- The test harness window shim lacked addEventListener, which the 0.1.7
  primitives call at module-evaluation time; and the e2e token exchange
  needs Playwright's request stack, not Node's fetch.
This commit is contained in:
Menghuan1918
2026-09-22 17:09:08 +08:00
parent de99fd4cfb
commit 6586e3e2d3
96 changed files with 4567 additions and 2080 deletions
+109 -58
View File
@@ -148,38 +148,12 @@ export interface SidebarSessionSummary {
running?: boolean
}
/** One healthy subagent catalog child row (structural mirror of the runtime). */
export interface SidebarSubagentChildEntry {
kind: 'child'
id: string
/** Whether the child Agent driver is running at the Host sampling boundary. */
activity: 'running' | 'inactive'
/** Whether a direct descendant has durable `origin: 'subagent'`. */
hasChildren: boolean
mode: 'one-shot' | 'continuable'
label?: string
}
/** One unreadable catalog row (corrupt / unsupported / unavailable). */
export interface SidebarSubagentDiagnosticEntry {
kind: 'diagnostic'
id: string
reason: 'corrupt' | 'unsupported' | 'unavailable'
}
/** The per-parent lazy catalog delivered through the sessions list feed. */
export interface SidebarSubagentCatalog {
entries: Array<SidebarSubagentChildEntry | SidebarSubagentDiagnosticEntry>
parentAvailable: boolean
state: 'loading' | 'ready' | 'error'
error: { code?: string; message?: string } | null
}
/** Durable parent/child address that selects subagent transport in the client. */
export interface SidebarSubagentAddress {
parentSessionId: string
childSessionId: string
mode: 'one-shot' | 'continuable'
/** `unknown` keeps a child visible without claiming continuation support. */
mode: 'one-shot' | 'continuable' | 'unknown'
}
/** Minimal structural mirror of one session event (the subagent history tail). */
@@ -220,10 +194,21 @@ export interface SidebarJobView {
finishedAt?: number
}
/** The host jobs registry face the sidebar routes touch (structural mirror of `JobRegistry`). */
/**
* The host jobs registry face the sidebar routes touch (structural mirror of
* `JobRegistry`).
*
* DSH 0.1.7 moved the access fence from the live `Agent` to the reading
* session id: `caller` is a `SessionId` string on every member. The plugin no
* longer needs `ctx.agents` in order to call the registry, and the Tasks page
* reads `list` through the plugin's own `jobs.list` route (the client session
* snapshot stopped mirroring background jobs in the same release).
*/
export interface SidebarJobsService {
/** Caller-owned and unowned jobs in registration order (the Tasks page list). */
list(caller?: string): SidebarJobView[]
/** Request cancellation; throws for an unknown or foreign job. */
kill(id: string, caller?: SidebarAgent, reason?: string): 'requested' | 'already-finished'
kill(id: string, caller?: string, reason?: string): 'requested' | 'already-finished'
}
/** The host agent registry face (structural mirror of the runtime `ctx.agents`). */
@@ -328,16 +313,37 @@ export interface SidebarSessionHandle {
/** The client session list snapshot the sidebar subscribes to. */
export interface SidebarSessionList {
current: string | undefined
byId: Record<string, SidebarSessionSummary>
/** Direct durable catalogs keyed by their selected parent address. */
subagentsByParent?: Readonly<Record<string, SidebarSubagentCatalog>>
/**
* Background jobs per session, last-wins from the harness's `session/jobs`
* push (a missing key is an empty set). Absent on runtime snapshots older
* than the jobs mirror — the sidebar simply shows no job rows.
* Host-computed projection values per session (DSH 0.1.7). The only field
* this plugin reads is `subagentCatalog`, the direct-child list the 0.1.6
* runtime published as `subagentsByParent`.
*
* Two facts the 0.1.6 snapshot carried are gone and must not be re-read:
* there is no `current` session id (`ctx.sidebarRight.mounted` is the
* sanctioned feed for "which session's seat is on screen"), and there is no
* background-jobs mirror (the `jobs.list` route reads the registry itself).
*/
jobsBySession?: Readonly<Record<string, readonly SidebarJobView[]>>
projectionsBySession?: Readonly<Record<string, SidebarProjectionSnapshot>>
}
/** One session's projection values, as the client snapshot publishes them. */
export interface SidebarProjectionSnapshot {
values: { subagentCatalog?: readonly SidebarSubagentCatalogEntry[] }
state: 'idle' | 'loading' | 'ready' | 'error'
error: { code?: string; message?: string } | null
}
/** One direct-child row of the host's `subagentCatalog` projection. */
export interface SidebarSubagentCatalogEntry {
/** Child session id (`childId` in the durable event). */
id: string
/** Epoch ms the child was created. */
createdAt: number
/** `unknown` keeps a child visible without claiming continuation support. */
mode: 'one-shot' | 'continuable' | 'unknown'
/** Mode-specific label; continuable children always carry one. */
label?: string
}
/** The client sessions service face (only the list feed is needed). */
@@ -442,33 +448,64 @@ export interface SidebarInvariantsService {
): () => void
}
/** The settings service face (mirror of @deepseek-ai/dsh-settings' SettingsProvider). */
/**
* The settings service face (mirror of `@deepseek-ai/dsh-settings`'
* `SettingsForms`).
*
* DSH 0.1.7 replaced the registrable-namespace provider with a forms service
* over the profile's own entries: a form is addressed by the **Loader entry
* id** of the plugin row (`better-sidebar` for this bundle's patch, whatever
* id an aggregate bundle mounted it under), its schema is the row's exported
* `Config`, and the value shown is the live fiber config. There is no
* `register`/`get`/`watch` any more — reads go through {@link describe} and
* writes through {@link update}, which also carries the revision guard.
*/
export interface SidebarSettingsService {
/**
* Register one namespace schema (the resolved value layers schema defaults,
* then the composition base, then the user document).
*/
register<T>(
ns: string,
schema: unknown,
options?: { base?: Partial<T>; applies?: 'live' | 'restart' },
): {
get(): T
watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
update(patch: object): Promise<void>
replace(section: object): Promise<void>
}
/** Redacted descriptors of every registered namespace (secrets stripped). */
/** Redacted descriptors of every configurable profile entry (secrets stripped). */
describe(options?: { redactSecrets?: boolean }): Array<{
/** Profile entry id — NOT a plugin-chosen namespace. */
ns: string
value?: unknown
base?: unknown
user?: unknown
applies: 'live' | 'restart'
revision: number
value?: unknown
/**
* The profile override layer alone, projected through the form. Empty
* means nothing has been persisted for this entry yet, which is what the
* one-time legacy import tests before seeding. Absent under
* `redactSecrets`.
*/
user?: unknown
}>
/** Service-level merge write with the revision guard (a stale writer is refused). */
/** Merge editable fields into one entry's config (a stale writer is refused). */
update(ns: string, patch: object, expectedRevision?: number): Promise<void>
/**
* Opt this plugin instance out of the auto-generated page: the plugin ships
* its own Side card settings section, so the native form must not duplicate
* it. The policy does not remove configuration reads or writes.
* @param presentation - page policy; `auto: false` opts out.
* @param owner - the plugin instance's fiber (the loader entry's fiber).
* @returns Disposer; register it with the plugin's effects.
*/
configure(presentation: { auto?: boolean }, owner?: unknown): () => void
}
/**
* The Loader face this plugin consumes (structural mirror of the loader's
* `Loader#entries`). Each entry carries the row's configured id/name and the
* fiber it owns, which together identify this plugin's own row.
*/
export interface SidebarLoaderService {
entries(): Iterable<SidebarLoaderEntry>
}
/** One configured plugin row inside the profile's entry tree. */
export interface SidebarLoaderEntry {
options: {
id?: string
name?: string
}
/** The fiber the row owns once it is loaded; compared by identity. */
fiber?: unknown
disabled?: boolean
}
/**
@@ -524,6 +561,20 @@ export interface SidebarContextShape {
jobs: SidebarJobsService
/** The host live-agent registry (optional; side chat thread agents). */
agents: SidebarAgentsService
/**
* The active profile's context (optional). Only `home` is consumed: the
* harness home is where DSH 0.1.7 leaves the removed `settings.yaml` under
* its `.imported` name, which is the one surviving source of a pre-0.1.7
* user's Side card preferences.
*/
profileContext?: { home: string }
/**
* The Loader's entry list. This plugin reads it only to discover the id its
* own row was mounted under — 0.1.7 addresses settings forms by profile
* entry id, and the id is not knowable at author time (an aggregate bundle
* mounts the same package under its own id).
*/
loader: SidebarLoaderService
/** The host subagent runtime (optional; live topology batch route). */
subagents: SidebarSubagentsService
/** The host agent-presets service (optional; side chat cold resume). */