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
+4 -4
View File
@@ -12,7 +12,7 @@
* from the chat's http(s) links) owns embedded pages. See
* docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md.
*/
import { IconCodeOutline16, IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconCodeOutlineRegular, IconPanelLeftOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import {
changesTabIcon, filesTabIcon, sidechatTabIcon, tasksTabIcon,
} from './tab-icons.tsx'
@@ -27,7 +27,7 @@ import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideCh
import { api } from '../api.ts'
import type { TabDescriptor } from '../service.ts'
/** The 6 built-in tab descriptors. */
/** The 5 built-in tab descriptors (the host owns terminal and browser). */
export function builtinTabs(): readonly TabDescriptor[] {
return [
{
@@ -57,13 +57,13 @@ export function builtinTabs(): readonly TabDescriptor[] {
options: [
{
value: true,
icon: (size: number) => <IconPanelLeftOutline16 size={size} />,
icon: (size: number) => <IconPanelLeftOutlineRegular size={size} />,
title: () => t('editorExplorerMerged'),
desc: () => t('editorExplorerMergedDesc'),
},
{
value: false,
icon: (size: number) => <IconCodeOutline16 size={size} />,
icon: (size: number) => <IconCodeOutlineRegular size={size} />,
title: () => t('editorExplorerSplit'),
desc: () => t('editorExplorerSplitDesc'),
},
+23 -54
View File
@@ -1,41 +1,44 @@
/**
* The 6 built-in file viewer descriptors: every preview surface is a
* registered viewer (image / pdf / markdown / html / code /
* binary-download), exactly like external plugins register theirs. Office
* previews (.docx / .xlsx / .pptx) are NOT built in anymore — they moved to
* the recommended office plugin (see plugins-viewers.ts), which registers
* the same ids through this service.
* The 3 built-in file viewer descriptors (markdown / html / code), exactly
* like external plugins register theirs.
*
* The `binary-download` viewer sniffs NUL bytes via `detect` for unknown
* binaries and serves legacy doc/xls/ppt by extension; `code` is the
* catch-all (`exts: []`, lowest priority) that claims any file no other
* viewer did.
* DSH 0.1.7 ships `ui-sidebar-documentpreview`, its own code / spreadsheet /
* office / pdf / image / html / markdown / text previews with zoom and
* auto-refresh, so this plugin yields every READ-ONLY preview it used to own
* and keeps only the surfaces where it is not equivalent:
* - `markdown` — this plugin's own renderer (front matter, embedded HTML
* sanitation, floating TOC), which the user prefers to the host's;
* - `html` — the sandboxed iframe preview plus the host-less
* `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` escape hatches;
* - `code` — the catch-all (`exts: []`, lowest priority) that claims any
* file no other viewer did, and it is an EDITABLE CodeMirror with save —
* the host's equivalents are read-only previews.
* The yielded ids (image / pdf / binary-download) are deliberately NOT
* registered here; an external plugin may still claim them through this
* service. Office previews (.docx / .xlsx / .pptx) were already not built
* in — they live in the recommended office plugin (see plugins-viewers.ts),
* which registers the same ids through this service.
*
* The heavy viewers (the CodeMirror-backed markdown/html/code) render
* through {@link lazyChunkComponent} wrappers — their libraries are fetched
* only when such a file is first opened (see chunk-loader.ts). The
* descriptor metadata (id/exts/priority/detect) is identical either way,
* so matching semantics and external-plugin overrides are unaffected; the
* descriptor metadata (id/exts/priority) is identical either way, so
* matching semantics and external-plugin overrides are unaffected; the
* `component` wrapper keeps the descriptor contract `(props) => ReactNode`.
*
* Every viewer carries the declarative settings-surface fields — `title`
* and `icon` — so the Side card settings page can render the enable/disable
* inventory without hardcoding (eating our own dogfood).
*/
import { IconCodeOutline16, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconCodeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
import { lazyChunkComponent } from '../lazy-chunk.tsx'
import { PdfView } from '../PdfView.tsx'
import { BinaryDownload } from '../binary-download.tsx'
import {
IconImageOutline16,
IconMarkdownOutline16,
IconPdfOutline16,
IconHtmlOutline16,
} from '../icons.tsx'
import type { ComponentType } from 'react'
import type { FileViewerDescriptor, FileViewerProps } from '../service.ts'
import { t } from '../locales.ts'
import css from '../sidebar.module.css'
/**
* Lazy wrapper over the chunk-resident viewer component. The `pick`
@@ -45,31 +48,9 @@ import css from '../sidebar.module.css'
*/
const LazyTextEditor = lazyChunkComponent<FileViewerProps>('editor', (mod) => mod.TextEditor as ComponentType<FileViewerProps> | undefined)
/** The 6 built-in file viewer descriptors. */
/** The 3 built-in file viewer descriptors. */
export function builtinViewers(): readonly FileViewerDescriptor[] {
return [
{
id: 'image',
title: () => t('viewerImage'),
icon: (size: number) => <IconImageOutline16 size={size} />,
exts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'],
fetchStrategy: 'mediaUrl',
component: ({ mediaUrl: url, title }) => (
<div className={css.editorImageWrap}>
<img className={css.editorImage} src={url} alt={title} />
</div>
),
},
{
id: 'pdf',
title: () => t('viewerPdf'),
icon: (size: number) => <IconPdfOutline16 size={size} />,
exts: ['pdf'],
fetchStrategy: 'mediaUrl',
component: ({ scope, path, title }) => (
<PdfView scope={scope} path={path} title={title} />
),
},
{
id: 'markdown',
title: () => t('viewerMarkdown'),
@@ -103,23 +84,11 @@ export function builtinViewers(): readonly FileViewerDescriptor[] {
{
id: 'code',
title: () => t('viewerCode'),
icon: (size: number) => <IconCodeOutline16 size={size} />,
icon: (size: number) => <IconCodeOutlineRegular size={size} />,
exts: [],
priority: -100,
fetchStrategy: 'fsRead',
component: (props) => <LazyTextEditor {...props} />,
},
{
id: 'binary-download',
title: () => t('viewerBinary'),
icon: (size: number) => <IconDownloadOutline16 size={size} />,
exts: ['doc', 'xls', 'ppt'],
priority: -50,
fetchStrategy: 'binary-download',
// NUL probe: a file whose head bytes contain a NUL is binary — claimed
// before the catch-all code viewer on the head re-match.
detect: (_path, head) => head.includes(0),
component: ({ scope, path }) => <BinaryDownload scope={scope} path={path} />,
},
]
}