dsh-coding-sidebar 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +27 -42
  2. package/lib/client-editor.js +449 -4278
  3. package/lib/client-registry.js +641 -1343
  4. package/lib/client-terminal.js +172 -262
  5. package/lib/client.js +645 -1347
  6. package/lib/index.js +24 -122
  7. package/lib/types/bundle-route.d.ts +1 -1
  8. package/lib/types/client/SideCardSection.d.ts +3 -3
  9. package/lib/types/client/TextEditor.d.ts +20 -10
  10. package/lib/types/client/add-plugin-modal.d.ts +0 -4
  11. package/lib/types/client/api.d.ts +8 -11
  12. package/lib/types/client/builtins/index.d.ts +10 -10
  13. package/lib/types/client/chunk-loader.d.ts +7 -8
  14. package/lib/types/client/locales.d.ts +0 -26
  15. package/lib/types/client/native-avoid.d.ts +2 -0
  16. package/lib/types/client/plugins-shared.d.ts +6 -6
  17. package/lib/types/client/service.d.ts +13 -83
  18. package/lib/types/context-types.d.ts +19 -10
  19. package/lib/types/index.d.ts +1 -1
  20. package/lib/types/prefs-shared.d.ts +1 -24
  21. package/package.json +26 -30
  22. package/src/bundle-route.ts +1 -1
  23. package/src/client/EditorHost.tsx +52 -122
  24. package/src/client/SandboxStatusBar.tsx +12 -9
  25. package/src/client/SideCardSection.module.css +2 -2
  26. package/src/client/SideCardSection.tsx +28 -81
  27. package/src/client/SideChatView.module.css +12 -12
  28. package/src/client/TextEditor.tsx +89 -266
  29. package/src/client/add-plugin-modal.tsx +16 -24
  30. package/src/client/api.ts +1 -25
  31. package/src/client/builtins/index.ts +10 -14
  32. package/src/client/chunk-loader.ts +7 -8
  33. package/src/client/index.tsx +12 -3
  34. package/src/client/locales-ar.ts +0 -6
  35. package/src/client/locales-de.ts +0 -6
  36. package/src/client/locales-fr.ts +0 -6
  37. package/src/client/locales-hi.ts +0 -6
  38. package/src/client/locales-id.ts +0 -6
  39. package/src/client/locales-it.ts +0 -6
  40. package/src/client/locales-ja.ts +0 -6
  41. package/src/client/locales-ko.ts +0 -6
  42. package/src/client/locales-nl.ts +0 -6
  43. package/src/client/locales-pl.ts +0 -6
  44. package/src/client/locales-pt.ts +0 -6
  45. package/src/client/locales-ru.ts +0 -6
  46. package/src/client/locales-sv.ts +0 -6
  47. package/src/client/locales-th.ts +0 -6
  48. package/src/client/locales-tr.ts +0 -6
  49. package/src/client/locales-vi.ts +0 -6
  50. package/src/client/locales-zh-HK.ts +0 -6
  51. package/src/client/locales-zh-MO.ts +0 -6
  52. package/src/client/locales-zh-TW.ts +0 -6
  53. package/src/client/locales.ts +0 -52
  54. package/src/client/native-avoid.ts +155 -0
  55. package/src/client/plugins-shared.ts +6 -6
  56. package/src/client/prefs.ts +0 -7
  57. package/src/client/service.ts +13 -150
  58. package/src/client/sidebar.module.css +1 -425
  59. package/src/config.ts +1 -4
  60. package/src/context-types.ts +12 -7
  61. package/src/index.ts +12 -78
  62. package/src/jobs-routes.ts +3 -2
  63. package/src/prefs-shared.ts +1 -27
  64. package/src/sidechat-routes.ts +13 -8
  65. package/src/subagent-live-route.ts +2 -1
  66. package/lib/client-mermaid.js +0 -200753
  67. package/lib/types/client/MarkdownHtml.d.ts +0 -32
  68. package/lib/types/client/PdfView.d.ts +0 -6
  69. package/lib/types/client/builtins/viewers.d.ts +0 -3
  70. package/lib/types/client/chunks/mermaid.d.ts +0 -10
  71. package/lib/types/client/editor-load.d.ts +0 -66
  72. package/lib/types/client/image-types.d.ts +0 -3
  73. package/lib/types/client/markdown-html.d.ts +0 -96
  74. package/lib/types/client/markdown-images.d.ts +0 -45
  75. package/lib/types/client/md-toc.d.ts +0 -4
  76. package/lib/types/client/mermaid-blocks.d.ts +0 -47
  77. package/lib/types/client/mermaid-sanitize.d.ts +0 -2
  78. package/lib/types/client/mermaid.d.ts +0 -12
  79. package/lib/types/client/pdf-types.d.ts +0 -2
  80. package/lib/types/client/plugins-viewers.d.ts +0 -3
  81. package/lib/types/html-route.d.ts +0 -59
  82. package/src/client/MarkdownHtml.tsx +0 -296
  83. package/src/client/PdfView.tsx +0 -110
  84. package/src/client/builtins/viewers.tsx +0 -125
  85. package/src/client/chunks/mermaid.tsx +0 -10
  86. package/src/client/editor-load.ts +0 -92
  87. package/src/client/image-types.ts +0 -8
  88. package/src/client/markdown-html.ts +0 -331
  89. package/src/client/markdown-images.ts +0 -137
  90. package/src/client/md-toc.tsx +0 -127
  91. package/src/client/mermaid-blocks.ts +0 -110
  92. package/src/client/mermaid-sanitize.ts +0 -93
  93. package/src/client/mermaid.tsx +0 -406
  94. package/src/client/pdf-types.ts +0 -4
  95. package/src/client/plugins-viewers.ts +0 -30
  96. package/src/html-route.ts +0 -106
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * The BetterSidebar client service: a registry that external plugins use
3
- * to contribute sidebar tab types and file previewers. The service is
4
- * published to the cordis context as `ctx.betterSidebar` (see
5
- * {@link ../context-types.ts}); consumers declare it in `inject` and call
6
- * `registerTab` / `registerFileViewer`, both returning a disposer that
7
- * cordis auto-invokes on fiber disposal (HMR-safe).
3
+ * to contribute sidebar tab types. The service is published to the cordis
4
+ * context as `ctx.betterSidebar` (see {@link ../context-types.ts});
5
+ * consumers declare it in `inject` and call `registerTab`, which returns a
6
+ * disposer that cordis auto-invokes on fiber disposal (HMR-safe).
7
+ *
8
+ * (v1.0.4: the file-viewer registry — `registerFileViewer` /
9
+ * `matchFileViewer` — was retired; file preview is the host's job now and
10
+ * the sidebar edits text only.)
8
11
  *
9
12
  * Design notes:
10
13
  * - The registry is synchronous-snapshot (Map + listener set) so React
@@ -15,9 +18,6 @@
15
18
  * `single: true` is sugar for `dedupeKey: () => id`.
16
19
  * - `createTab` lets a descriptor own tab instantiation (the terminal
17
20
  * builtin uses it to mint `terminal:<n>` ids and bump `nextTerminal`).
18
- * - `matchFileViewer` walks descriptors in priority order (desc, stable):
19
- * per descriptor it tries `detect` first (when `head` bytes are given),
20
- * then `exts`; `exts: []` is a catch-all that matches any path.
21
21
  */
22
22
  import type { ReactNode } from 'react';
23
23
  import type { Context } from '../context-types.ts';
@@ -50,7 +50,7 @@ export interface SidebarSettingSelectOption {
50
50
  * option's icon too; without icons both are a single line of text. */
51
51
  icon?: ReactNode | ((size: number) => ReactNode);
52
52
  }
53
- /** One declarative setting of a tab/viewer, rendered as a nested row in the
53
+ /** One declarative setting of a registered tab, rendered as a nested row in the
54
54
  * Side card settings page (e.g. the Subagent page's "auto-open when a
55
55
  * subagent appears" switch, or the terminal's custom font rows). `type`
56
56
  * selects the control: 'switch' (default) renders the custom switch,
@@ -93,7 +93,7 @@ export interface SidebarSettingsRenderProps {
93
93
  /** Close the settings popup. */
94
94
  close(): void;
95
95
  }
96
- /** Declarative settings of one registered tab or file viewer. */
96
+ /** Declarative settings of one registered tab. */
97
97
  export interface SidebarSettingsDeclaration {
98
98
  /**
99
99
  * Extra settings rows rendered under the feature's own row in the
@@ -223,78 +223,17 @@ export interface TabDescriptor {
223
223
  onClose?: (tab: SidebarTab, scope: SessionScope) => void;
224
224
  component: (props: TabComponentProps) => ReactNode;
225
225
  }
226
- /** How the host loads a file's bytes for one viewer. */
227
- export type FileFetchStrategy = 'none' | 'fsRead' | 'mediaUrl' | 'custom' | 'binary-download';
228
- /** Props every file viewer component receives. */
229
- export interface FileViewerProps {
230
- ctx: Context;
231
- store: SidebarStore;
232
- scope: SessionScope;
233
- path: string;
234
- title: string;
235
- /** The matching descriptor's id (`'code'`, `'my-plugin:csv'`). */
236
- viewerId: string;
237
- /** fsRead text content (fetchStrategy='fsRead'). */
238
- content?: string;
239
- truncated?: boolean;
240
- /** mediaUrl for the path (fetchStrategy='mediaUrl'). */
241
- mediaUrl?: string;
242
- /** custom load() return value (fetchStrategy='custom'). */
243
- customData?: unknown;
244
- /** Internal (built-in text editor): 'host' asks the viewer to skip its own
245
- * toolbar row — the editor host's merged-mode header renders it instead,
246
- * fed through the two callbacks below. Viewers that ignore these fields
247
- * render exactly as before. */
248
- toolbar?: 'self' | 'host';
249
- /** Internal: the viewer reports its toolbar state (mode/dirty/save). */
250
- onToolbarState?: (state: EditorToolbarState) => void;
251
- /** Internal: the viewer registers its toolbar commands on mount (null on
252
- * unmount). */
253
- onToolbarControls?: (controls: EditorToolbarControls | null) => void;
254
- }
255
- /** The toolbar state a text editor reports to the host's merged-mode header. */
226
+ /** The toolbar state the text editor reports to the host's merged-mode header. */
256
227
  export interface EditorToolbarState {
257
- /** Whether the preview/edit mode toggle applies (markdown/html). */
258
- modes: boolean;
259
- mode: 'preview' | 'edit';
260
228
  dirty: boolean;
261
229
  /** Whether saving applies (text content loaded). */
262
230
  editable: boolean;
263
231
  saveState: 'idle' | 'saving' | 'saved' | 'failed';
264
232
  }
265
- /** The commands the host's merged-mode header sends back to the viewer. */
233
+ /** The commands the host's merged-mode header sends back to the editor. */
266
234
  export interface EditorToolbarControls {
267
- setMode(mode: 'preview' | 'edit'): void;
268
235
  save(): void;
269
236
  }
270
- /** Describes one file previewer (builtins register themselves too). */
271
- export interface FileViewerDescriptor {
272
- /** Unique id (`'image'`, `'pdf'`, `'my-plugin:csv'`). */
273
- id: string;
274
- /** Display name for the settings inventory (falls back to `id` when absent). */
275
- title?: string | (() => string);
276
- /** Icon shown in the settings inventory. */
277
- icon?: ReactNode | ((size: number) => ReactNode);
278
- /** Lowercase extensions without leading dot (`['png','jpg']`). `[]` = match any (catch-all). */
279
- exts: readonly string[];
280
- /** Higher wins; default 0. Builtins use 0; the catch-all `code` viewer uses -100. */
281
- priority?: number;
282
- fetchStrategy: FileFetchStrategy;
283
- /**
284
- * Content sniff: when `head` bytes are available the descriptor's `detect`
285
- * is consulted before its `exts` (per-descriptor, in priority order).
286
- */
287
- detect?: (path: string, head: Uint8Array) => boolean;
288
- /** fetchStrategy='custom' loader. `signal` (v0.12.0+) aborts on viewer
289
- * teardown / re-match; loaders that ignore it keep working. */
290
- load?: (path: string, scope: SessionScope, signal?: AbortSignal) => Promise<unknown>;
291
- /**
292
- * Declarative settings shown in the Side card settings page: every
293
- * registered viewer gets an enable/disable switch (icon + title + exts).
294
- */
295
- settings?: SidebarSettingsDeclaration;
296
- component: (props: FileViewerProps) => ReactNode;
297
- }
298
237
  /** One `openTab` request. */
299
238
  export interface OpenTabSeed {
300
239
  type: string;
@@ -316,9 +255,7 @@ export interface OpenTabSeed {
316
255
  */
317
256
  export interface BetterSidebarService {
318
257
  registerTab(descriptor: TabDescriptor): () => void;
319
- registerFileViewer(descriptor: FileViewerDescriptor): () => void;
320
258
  getTabs(): readonly TabDescriptor[];
321
- getFileViewers(): readonly FileViewerDescriptor[];
322
259
  /** Find a tab descriptor by id (undefined if not registered). */
323
260
  getTab(id: string): TabDescriptor | undefined;
324
261
  /**
@@ -328,13 +265,6 @@ export interface BetterSidebarService {
328
265
  * derived flows gate on it).
329
266
  */
330
267
  isTabEnabled(id: string): boolean;
331
- /** Whether a file viewer is enabled (absent `viewersEnabled[id]` = enabled). */
332
- isViewerEnabled(id: string): boolean;
333
- /**
334
- * Find a file viewer for a path (priority desc; detect first, then exts).
335
- * Disabled viewers are skipped, so files fall through to the next match.
336
- */
337
- matchFileViewer(path: string, head?: Uint8Array): FileViewerDescriptor | undefined;
338
268
  /**
339
269
  * Open a tab (used by external tabs and the + menu). `title` overrides
340
270
  * the descriptor's title when given (the editor tab shows the file name);
@@ -433,7 +363,7 @@ export declare const SIDEBAR_SERVICE_VERSION: string;
433
363
  export declare const SIDEBAR_FEATURES: readonly ["badge", "tabLifecycle", "updateTab", "openFile", "targetedOpen", "stateSubscription", "tabMeta", "pluginSettings", "urlTarget", "settingSelect", "floatWindows"];
434
364
  /**
435
365
  * Create one BetterSidebar service bound to a store. The service owns the
436
- * tab/viewer registries (Map + listener set) and proxies openTab/closeTab
366
+ * tab registry (Map + listener set) and proxies openTab/closeTab
437
367
  * to the store's reducer. One instance per client plugin activation.
438
368
  */
439
369
  export declare function createBetterSidebarService(store: SidebarStore): BetterSidebarService;
@@ -78,11 +78,11 @@ export interface SidebarSessionStore {
78
78
  get(id: string): {
79
79
  header: SidebarSessionHeader;
80
80
  /**
81
- * The live session's append-only event log (immutable snapshot; absent
81
+ * The live session's immutable event snapshot (0.1.5 Session API; absent
82
82
  * on sessions the runtime has not hydrated). Read-only access — the
83
83
  * jobs.output route replays `job_output` tool/result rows from it.
84
84
  */
85
- events?: readonly SidebarSessionEvent[];
85
+ snapshotEvents?(fromSeq?: number, toSeqExclusive?: number): readonly SidebarSessionEvent[];
86
86
  } | undefined;
87
87
  }
88
88
  /**
@@ -276,16 +276,25 @@ export interface SidebarSessionTitleService {
276
276
  };
277
277
  }
278
278
  /** The host session-persistence face (mirror of the sessionPersistence
279
- * service): detached inspection of a persisted session, used to compose the
280
- * recorded preset when a Side Chat thread cold-resumes. */
281
- export interface SidebarSessionPersistenceService {
282
- inspect(sessionId: string): Promise<{
283
- meta: {
284
- cwd?: string;
285
- agentPreset?: string;
286
- };
279
+ * service, 0.1.5 handle-seam form): a short-lived read handle over one
280
+ * persisted session, used to compose the recorded preset when a Side Chat
281
+ * thread cold-resumes. */
282
+ export interface SidebarSessionPersistenceHandle {
283
+ readonly header: {
284
+ cwd?: string;
285
+ agentPreset?: string;
286
+ };
287
+ read(fromSeq?: number, toSeqExclusive?: number, options?: {
288
+ signal?: AbortSignal;
289
+ }): Promise<{
287
290
  events: readonly SidebarSessionEvent[];
288
291
  }>;
292
+ close(): Promise<void>;
293
+ }
294
+ export interface SidebarSessionPersistenceService {
295
+ open(sessionId: string, access: 'read', options?: {
296
+ signal?: AbortSignal;
297
+ }): Promise<SidebarSessionPersistenceHandle>;
289
298
  }
290
299
  /** RPC result slot mirror (`RpcResult<T>` on the wire). */
291
300
  export type SidebarRpcResult<T> = {
@@ -3,7 +3,7 @@ import { Config, type ResolvedSidebarConfig, type SidebarConfig } from './config
3
3
  export { Config };
4
4
  export type { SidebarConfig, ResolvedSidebarConfig };
5
5
  export type { Context } from './context-types.ts';
6
- export type { BetterSidebarService, TabDescriptor, TabComponentProps, FileViewerDescriptor, FileViewerProps, FileFetchStrategy, } from './client/service.ts';
6
+ export type { BetterSidebarService, TabDescriptor, TabComponentProps, } from './client/service.ts';
7
7
  /** Plugin identity for cordis.yml rows. */
8
8
  export declare const name = "dsh-coding-sidebar";
9
9
  /** Services required before mounting: the webserver routes, the session store, the web runtime's trusted hosts, and the tool registry. */
@@ -124,22 +124,6 @@ export interface SidebarPrefs {
124
124
  * starts `strip` px below its top edge.
125
125
  */
126
126
  titleBarStripPx: number;
127
- /**
128
- * Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the
129
- * default) renders previewed HTML in an opaque-origin iframe that cannot
130
- * touch the GUI; turning it OFF runs the previewed page with the GUI's
131
- * own origin — full read/write access to session files and internal
132
- * APIs. Only for trusted local content; the setting copy warns.
133
- */
134
- htmlViewerNoSandbox: boolean;
135
- /**
136
- * Whether a newly opened HTML preview starts UNSANDBOXED (the per-surface
137
- * temporary unlock pre-applied). Off by default: previews open sandboxed
138
- * and the status row offers the one-tap unlock; when on, previews open
139
- * in the red unsandboxed state and the status row offers a one-tap
140
- * restore for the current file.
141
- */
142
- htmlViewerDefaultUnsafe: boolean;
143
127
  /**
144
128
  * Whether the browser tab drops its sandboxed iframe. Sandbox ON (the
145
129
  * default) keeps browsed sites in an opaque origin with no GUI access;
@@ -194,16 +178,9 @@ export interface SidebarPrefs {
194
178
  * own layouts" rule.
195
179
  */
196
180
  tabsEnabled: Record<string, boolean>;
197
- /**
198
- * Per-viewer enable switches, keyed by file viewer descriptor id
199
- * (`'image'`, `'my-plugin:csv'`). An ABSENT key means enabled; a disabled
200
- * viewer is skipped by `matchFileViewer` so files fall through to the
201
- * next matching viewer (or the download button when none match).
202
- */
203
- viewersEnabled: Record<string, boolean>;
204
181
  /**
205
182
  * Plugin-owned settings blobs (v0.12.0+), keyed by descriptor id: each
206
- * registered tab/viewer that declares `settings.pluginToggles` (or writes
183
+ * registered tab that declares `settings.pluginToggles` (or writes
207
184
  * through `settings.render`'s `updatePluginSetting`) persists its values
208
185
  * here — an open map, so third-party keys need no host PrefsSchema field.
209
186
  * Values are JSON-serializable (the row controls produce strings /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-coding-sidebar",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -61,7 +61,6 @@
61
61
  "lib/client-registry.js",
62
62
  "lib/client-terminal.js",
63
63
  "lib/client-editor.js",
64
- "lib/client-mermaid.js",
65
64
  "lib/types/**/*.d.ts",
66
65
  "src",
67
66
  "scripts/install.sh",
@@ -85,19 +84,18 @@
85
84
  "license": "MIT",
86
85
  "peerDependencies": {
87
86
  "@deepseek-ai/cordis": "^4.0.1",
88
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
89
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
90
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
91
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
92
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
93
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
94
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
95
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
96
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
97
- "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
98
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
99
- "@deepseek-ai/dsh-subagent": "^0.1.0-rc.8",
100
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
87
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
88
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
101
99
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
102
100
  "react": "^18.2.0",
103
101
  "react-dom": "^18.2.0"
@@ -131,8 +129,6 @@
131
129
  "@codemirror/view": "^6.43.8",
132
130
  "@lezer/highlight": "^1.2.3",
133
131
  "clsx": "^2.1.1",
134
- "dompurify": "^3.4.14",
135
- "mermaid": "^11.16.1",
136
132
  "node-pty": "^1.1.0",
137
133
  "react-icons": "5.7.0",
138
134
  "rxjs": "^7.8.2",
@@ -140,20 +136,20 @@
140
136
  "ws": "^8.18.0"
141
137
  },
142
138
  "devDependencies": {
139
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
143
140
  "@cordisjs/plugin-loader": "^1.0.0-rc.5",
144
- "@deepseek-ai/dsh-agent": "0.1.2-alpha.2",
145
- "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2",
146
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.2",
147
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
148
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2",
149
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2",
150
- "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
151
- "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2",
152
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
153
- "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
154
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
155
- "@deepseek-ai/dsh-subagent": "0.1.2-alpha.2",
156
- "@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
141
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.2",
142
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
143
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
144
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
145
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
146
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
147
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
148
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
149
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2",
150
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
151
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
152
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
157
153
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
158
154
  "@types/node": "^24.0.0",
159
155
  "@types/react": "~18.3.1",
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'
19
19
  import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
20
20
 
21
21
  /** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
22
- export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid'] as const
22
+ export const CHUNK_NAMES = ['terminal', 'editor'] as const
23
23
  export type ChunkName = (typeof CHUNK_NAMES)[number]
24
24
 
25
25
  /** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
@@ -1,10 +1,11 @@
1
1
  /**
2
- * The editor tab host: the single FILES WINDOW. It resolves a file's
3
- * previewer through the sidebar registry (`matchFileViewer`), fetches bytes
4
- * per the matched viewer's fetch strategy, and renders its component — or
5
- * the shared download pane when nothing can render the file. A tab without
6
- * a path (the seeded "Files" home) renders an empty-state hint instead of
7
- * the viewer loading flow; that path-less window IS the file explorer.
2
+ * The editor tab host: the single FILES WINDOW. It fetches a file's text
3
+ * through the host fs.read — text renders in the (chunk-lazy) sidebar text
4
+ * editor, a binary file renders the shared download pane. (v1.0.4: the
5
+ * file-viewer registry/matching was retired file preview is the host's
6
+ * job now, the sidebar edits text only.) A tab without a path (the seeded
7
+ * "Files" home) renders an empty-state hint instead of the loading flow;
8
+ * that path-less window IS the file explorer.
8
9
  *
9
10
  * The chrome depends on the `editorExplorer` mode (read reactively so
10
11
  * toggling it re-renders without a reload):
@@ -18,18 +19,14 @@
18
19
  * The tree's context menu offers the explicit escapes in both modes: open
19
20
  * in a new tab (per-path dedupe) or to the side (a fresh tab in a fresh
20
21
  * rightward split of the current pane).
21
- *
22
- * The strategy dispatch is pure (planFirstMatch / planFsReadOutcome in
23
- * editor-load.ts); this component only wires it to the host APIs.
24
22
  */
25
23
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
26
- import { createElement } from 'react'
24
+ import type { ComponentType } from 'react'
27
25
  import clsx from 'clsx'
28
26
  import { IconCheckOutline16, IconFolderOpen16, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
29
27
  import type { Context } from '../context-types.ts'
30
- import { api, mediaUrl, type SessionScope } from './api.ts'
28
+ import { api, type SessionScope } from './api.ts'
31
29
  import { BinaryDownload } from './binary-download.tsx'
32
- import { planFirstMatch, planFsReadOutcome, type EditorLoadAction } from './editor-load.ts'
33
30
  import { baseName } from './FileTree.tsx'
34
31
  import { createFrameBatcher } from './frame-batcher.ts'
35
32
  import { openSidebarFile } from './intercept.tsx'
@@ -39,16 +36,27 @@ import { TreePanel } from './TreePanel.tsx'
39
36
  import { t } from './locales.ts'
40
37
  import { relativeTo } from './paths.ts'
41
38
  import { resolveSidebarPath } from './produced-files.ts'
42
- import type { EditorToolbarControls, EditorToolbarState, FileViewerDescriptor } from './service.ts'
39
+ import type { EditorToolbarControls, EditorToolbarState } from './service.ts'
40
+ import { lazyChunkComponent } from './lazy-chunk.tsx'
41
+ import type { TextEditorProps } from './TextEditor.tsx'
43
42
  import { firstLeaf, insertLeafAt, leafWithTab, mintTabId, treeOf, type SidebarStore, type SidebarTab } from './state.ts'
44
43
  import css from './sidebar.module.css'
45
44
 
46
45
  type EditorLoad =
47
46
  | { status: 'loading' }
48
47
  | { status: 'error'; message: string }
49
- | { status: 'ready'; viewer: FileViewerDescriptor; content?: string; truncated?: boolean; mediaUrl?: string; customData?: unknown }
48
+ | { status: 'ready'; content: string; truncated: boolean }
50
49
  | { status: 'binary' }
51
50
 
51
+ /**
52
+ * Lazy wrapper over the chunk-resident text editor: CodeMirror and the
53
+ * language packs are fetched only when a file tab first renders (see
54
+ * chunk-loader.ts). The `pick` function is module-level (stable identity —
55
+ * the wrapper effect depends on it); the cast bridges the chunk exports
56
+ * record to the prop shape.
57
+ */
58
+ const LazyTextEditor = lazyChunkComponent<TextEditorProps>('editor', (mod) => mod.TextEditor as ComponentType<TextEditorProps> | undefined)
59
+
52
60
  /** The docked tree panel's width bounds (drag-resize clamps into them). */
53
61
  const TREE_WIDTH_DEFAULT = 240
54
62
  const TREE_WIDTH_MIN = 160
@@ -103,11 +111,10 @@ export function EditorHost(props: {
103
111
  }) {
104
112
  const { ctx, store, scope, tab, expanded, revealed, onToggleDir, onReferenceFile } = props
105
113
  const path = tab.path ?? ''
106
- const title = tab.title
107
114
  // A folder window: the model's `sidebar_open` (or any caller) opens a
108
115
  // directory as an editor tab carrying `meta.dir: true` with the directory
109
116
  // as its path. It renders the file tree rooted at that folder instead of
110
- // the viewer loading flow (a directory is not a file).
117
+ // the file loading flow (a directory is not a file).
111
118
  const isDir = metaOf(tab).dir === true
112
119
  const [load, setLoad] = useState<EditorLoad>({ status: 'loading' })
113
120
  // Manual refresh (issue #167): bumping the sequence re-runs the load effect
@@ -223,9 +230,8 @@ export function EditorHost(props: {
223
230
  })
224
231
  }
225
232
 
226
- // The viewer's toolbar, hoisted into THIS header: the text editor reports
227
- // its state and registers its commands (both null/absent for viewers
228
- // without a toolbar — image, pdf, binary download).
233
+ // The editor's toolbar, hoisted into THIS header: the text editor reports
234
+ // its state and registers its commands (null while nothing is loaded).
229
235
  const [toolbar, setToolbar] = useState<EditorToolbarState | null>(null)
230
236
  const controlsRef = useRef<EditorToolbarControls | null>(null)
231
237
  const onToolbarState = useCallback((next: EditorToolbarState) => {
@@ -279,77 +285,27 @@ export function EditorHost(props: {
279
285
 
280
286
  useEffect(() => {
281
287
  // A (re)load or a path-less tab clears any hoisted toolbar state — the
282
- // fresh viewer re-registers its own.
288
+ // fresh editor re-registers its own.
283
289
  setToolbar(null)
284
- // The seeded home tab (no path) never loads a viewer — the empty-state
290
+ // The seeded home tab (no path) never loads a file — the empty-state
285
291
  // hint renders until the user picks a file. A folder tab never loads a
286
- // viewer either — its tree is rooted at the folder.
292
+ // file either — its tree is rooted at the folder.
287
293
  if (showEmpty || isDir) return
288
294
  let cancelled = false
289
- // Aborts the matched viewer's `load` when the editor tears down (tab
290
- // closed, path changed, session switched) or re-matches the viewer.
291
- const controller = new AbortController()
292
295
  setLoad({ status: 'loading' })
293
- const mediaUrlOf = (): string => mediaUrl(scope, path)
294
- const apply = (action: EditorLoadAction): void => {
296
+ api.fsRead(scope, path).then((result) => {
295
297
  if (cancelled) return
296
- switch (action.kind) {
297
- case 'binary':
298
- setLoad({ status: 'binary' })
299
- return
300
- case 'render':
301
- setLoad({
302
- status: 'ready',
303
- viewer: action.viewer,
304
- content: action.content,
305
- truncated: action.truncated,
306
- mediaUrl: action.mediaUrl,
307
- customData: action.customData,
308
- })
309
- return
310
- case 'customLoad':
311
- void action.viewer.load?.(path, scope, controller.signal).then((data) => {
312
- if (cancelled) return
313
- setLoad({ status: 'ready', viewer: action.viewer, customData: data })
314
- }).catch((error: unknown) => {
315
- if (cancelled) return
316
- setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
317
- })
318
- return
319
- case 'fetchFsRead':
320
- api.fsRead(scope, path).then((result) => {
321
- if (cancelled) return
322
- // Binary reads carry the head bytes for the detect re-match.
323
- const outcome = planFsReadOutcome(action.viewer, {
324
- binary: result.kind === 'binary',
325
- content: result.kind === 'text' ? result.content : '',
326
- truncated: result.truncated,
327
- head: result.kind === 'binary' ? result.head : undefined,
328
- }, (head) => ctx.get('betterSidebar')?.matchFileViewer(path, head), mediaUrlOf)
329
- apply(outcome)
330
- }).catch((error: unknown) => {
331
- if (cancelled) return
332
- setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
333
- })
334
- return
298
+ if (result.kind === 'binary') {
299
+ setLoad({ status: 'binary' })
300
+ return
335
301
  }
336
- }
337
- apply(planFirstMatch(ctx.get('betterSidebar')?.matchFileViewer(path), mediaUrlOf))
338
- return () => { cancelled = true; controller.abort() }
339
- }, [scope.sessionId, scope.cwd, path, ctx, showEmpty, isDir, reloadSeq])
340
-
341
- // Save-then-refresh in preview mode (issue #167 part C): the edge into
342
- // 'saved' (never a lingering 'saved' state) triggers exactly one reload, so
343
- // a preview-mode Ctrl+S shows the fresh content immediately. Edit mode is
344
- // left alone — reloading would remount the editor and drop the caret.
345
- const prevSaveState = useRef<EditorToolbarState['saveState'] | undefined>(undefined)
346
- useEffect(() => {
347
- const current = toolbar?.saveState
348
- if (prevSaveState.current !== 'saved' && current === 'saved' && toolbar?.mode === 'preview') {
349
- setReloadSeq(sequence => sequence + 1)
350
- }
351
- prevSaveState.current = current
352
- }, [toolbar?.saveState, toolbar?.mode])
302
+ setLoad({ status: 'ready', content: result.content, truncated: result.truncated })
303
+ }).catch((error: unknown) => {
304
+ if (cancelled) return
305
+ setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
306
+ })
307
+ return () => { cancelled = true }
308
+ }, [scope.sessionId, scope.cwd, path, showEmpty, isDir, reloadSeq])
353
309
 
354
310
  const treeOpen = treeOpenOf(tab)
355
311
  /** Persist the panel flag on the tab (survives reloads with the layout). */
@@ -392,33 +348,6 @@ export function EditorHost(props: {
392
348
  <div className={css.editor}>
393
349
  <div className={css.editorHeader}>
394
350
  <EditorPathInput key={path} path={path} cwd={scope.cwd} onOpen={openFile} />
395
- {toolbar?.modes === true && (
396
- <div className={css.editorModeToggle}>
397
- <button
398
- type="button"
399
- className={clsx(css.editorModeButton, toolbar.mode === 'preview' && css.editorModeActive)}
400
- onClick={() => {
401
- // Issue #167 part B: returning from edit to preview reloads so
402
- // the preview renders the just-saved content. A dirty draft
403
- // (or a failed save) suppresses the reload — the draft only
404
- // lives in the editor instance and a remount would drop it.
405
- if (toolbar.mode === 'edit' && toolbar.dirty !== true && toolbar.saveState !== 'failed') {
406
- setReloadSeq(sequence => sequence + 1)
407
- }
408
- controlsRef.current?.setMode('preview')
409
- }}
410
- >
411
- {t('preview')}
412
- </button>
413
- <button
414
- type="button"
415
- className={clsx(css.editorModeButton, toolbar.mode === 'edit' && css.editorModeActive)}
416
- onClick={() => { controlsRef.current?.setMode('edit') }}
417
- >
418
- {t('edit')}
419
- </button>
420
- </div>
421
- )}
422
351
  {toolbar?.dirty === true && <span className={css.dirtyDot} title={t('unsaved')} />}
423
352
  {toolbar?.editable === true && (
424
353
  <button
@@ -462,18 +391,19 @@ export function EditorHost(props: {
462
391
  {!showEmpty && load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
463
392
  {!showEmpty && load.status === 'error' && <div className={css.editorError}>{load.message}</div>}
464
393
  {!showEmpty && load.status === 'binary' && <BinaryDownload scope={scope} path={path} />}
465
- {!showEmpty && load.status === 'ready' && createElement(load.viewer.component, {
466
- ctx, store, scope, path, title,
467
- viewerId: load.viewer.id,
468
- content: load.content,
469
- truncated: load.truncated,
470
- mediaUrl: load.mediaUrl,
471
- customData: load.customData,
472
- // The viewer's toolbar always hoists into this host's header.
473
- toolbar: 'host',
474
- onToolbarState,
475
- onToolbarControls,
476
- })}
394
+ {!showEmpty && load.status === 'ready' && (
395
+ <LazyTextEditor
396
+ ctx={ctx}
397
+ store={store}
398
+ scope={scope}
399
+ path={path}
400
+ content={load.content}
401
+ truncated={load.truncated}
402
+ toolbar="host"
403
+ onToolbarState={onToolbarState}
404
+ onToolbarControls={onToolbarControls}
405
+ />
406
+ )}
477
407
  </div>
478
408
  {treeOpen && (
479
409
  <div className={css.editorTreeDock} style={{ width: treeWidth }}>
@@ -1,15 +1,18 @@
1
1
  /**
2
- * The live sandbox status row of the two built-in web surfaces (HTML
3
- * preview and the browser tab): a green "sandbox on" state with a one-tap
4
- * TEMPORARY unlock, or a RED "sandbox off" state (global setting or the
5
- * temporary unlock) with a restore action.
2
+ * The live sandbox status row of the built-in browser tab: a green
3
+ * "sandbox on" state with a one-tap TEMPORARY unlock, or a RED "sandbox
4
+ * off" state (global setting or the temporary unlock) with a restore
5
+ * action.
6
6
  *
7
7
  * The temporary unlock is component state only — it never writes the
8
- * global side card setting (`htmlViewerNoSandbox` / `browserNoSandbox`);
9
- * it lasts until the surface unmounts (tab switch / file switch) or the
10
- * user restores the sandbox from the row. When the global setting already
11
- * drops the sandbox, no unlock/restore action is offered (changing the
12
- * global setting is the settings page's job) — the red warning stands.
8
+ * global side card setting (`browserNoSandbox`); it lasts until the
9
+ * surface unmounts (tab switch) or the user restores the sandbox from the
10
+ * row. When the global setting already drops the sandbox, no
11
+ * unlock/restore action is offered (changing the global setting is the
12
+ * settings page's job) — the red warning stands.
13
+ *
14
+ * (v1.0.4: the HTML preview surface was retired with the file-viewer line;
15
+ * this row now serves the browser tab only.)
13
16
  */
14
17
  import clsx from 'clsx'
15
18
  import { t } from './locales.ts'