dsh-better-sidebar 0.10.0

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/README_EN.md +139 -0
  4. package/lib/client-docx.js +7649 -0
  5. package/lib/client-editor.js +32677 -0
  6. package/lib/client-pptx.js +63438 -0
  7. package/lib/client-registry.js +8021 -0
  8. package/lib/client-terminal.js +8060 -0
  9. package/lib/client-xlsx.js +359755 -0
  10. package/lib/client.js +8021 -0
  11. package/lib/index.js +2604 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-pty.d.ts +220 -0
  14. package/lib/types/browser-probe.d.ts +15 -0
  15. package/lib/types/bundle-route.d.ts +13 -0
  16. package/lib/types/client/BrowserView.d.ts +22 -0
  17. package/lib/types/client/DiffTab.d.ts +6 -0
  18. package/lib/types/client/DiffView.d.ts +51 -0
  19. package/lib/types/client/EditorHost.d.ts +10 -0
  20. package/lib/types/client/ExplorerView.d.ts +9 -0
  21. package/lib/types/client/GitView.d.ts +8 -0
  22. package/lib/types/client/OrphanedTab.d.ts +11 -0
  23. package/lib/types/client/PdfView.d.ts +6 -0
  24. package/lib/types/client/PptxView.d.ts +6 -0
  25. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  26. package/lib/types/client/SideCardSection.d.ts +27 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +38 -0
  30. package/lib/types/client/TerminalView.d.ts +8 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/api.d.ts +170 -0
  33. package/lib/types/client/binary-download.d.ts +12 -0
  34. package/lib/types/client/breakpoints.d.ts +12 -0
  35. package/lib/types/client/browser.d.ts +50 -0
  36. package/lib/types/client/builtins/index.d.ts +17 -0
  37. package/lib/types/client/builtins/tabs.d.ts +6 -0
  38. package/lib/types/client/builtins/viewers.d.ts +3 -0
  39. package/lib/types/client/chunk-loader.d.ts +74 -0
  40. package/lib/types/client/chunks/docx.d.ts +8 -0
  41. package/lib/types/client/chunks/editor.d.ts +9 -0
  42. package/lib/types/client/chunks/pptx.d.ts +8 -0
  43. package/lib/types/client/chunks/terminal.d.ts +9 -0
  44. package/lib/types/client/chunks/xlsx.d.ts +9 -0
  45. package/lib/types/client/cm-themes.d.ts +24 -0
  46. package/lib/types/client/conversation-draft.d.ts +14 -0
  47. package/lib/types/client/docx-view.d.ts +8 -0
  48. package/lib/types/client/editor-load.d.ts +66 -0
  49. package/lib/types/client/icons.d.ts +47 -0
  50. package/lib/types/client/image-types.d.ts +3 -0
  51. package/lib/types/client/ime-guard.d.ts +36 -0
  52. package/lib/types/client/index.d.ts +10 -0
  53. package/lib/types/client/intercept.d.ts +20 -0
  54. package/lib/types/client/lang.d.ts +12 -0
  55. package/lib/types/client/lazy-chunk.d.ts +25 -0
  56. package/lib/types/client/link-intercept.d.ts +35 -0
  57. package/lib/types/client/locales.d.ts +237 -0
  58. package/lib/types/client/office-shared.d.ts +33 -0
  59. package/lib/types/client/office-types.d.ts +36 -0
  60. package/lib/types/client/office-view.d.ts +12 -0
  61. package/lib/types/client/openpath-intercept.d.ts +41 -0
  62. package/lib/types/client/paths.d.ts +19 -0
  63. package/lib/types/client/pdf-types.d.ts +2 -0
  64. package/lib/types/client/prefs.d.ts +30 -0
  65. package/lib/types/client/produced-files.d.ts +27 -0
  66. package/lib/types/client/selection-payload.d.ts +27 -0
  67. package/lib/types/client/service.d.ts +214 -0
  68. package/lib/types/client/split-pane.d.ts +29 -0
  69. package/lib/types/client/state.d.ts +322 -0
  70. package/lib/types/client/subagent-activity.d.ts +35 -0
  71. package/lib/types/client/subagent-detect.d.ts +47 -0
  72. package/lib/types/client/subagent-jobs.d.ts +63 -0
  73. package/lib/types/client/theme.d.ts +28 -0
  74. package/lib/types/client/xlsx-to-univer.d.ts +33 -0
  75. package/lib/types/client/xlsx-view.d.ts +10 -0
  76. package/lib/types/config.d.ts +41 -0
  77. package/lib/types/context-types.d.ts +431 -0
  78. package/lib/types/fs-tree.d.ts +41 -0
  79. package/lib/types/git.d.ts +75 -0
  80. package/lib/types/html-route.d.ts +48 -0
  81. package/lib/types/index.d.ts +39 -0
  82. package/lib/types/invariant.d.ts +15 -0
  83. package/lib/types/jobs-routes.d.ts +45 -0
  84. package/lib/types/prefs-shared.d.ts +106 -0
  85. package/lib/types/pty-manager.d.ts +76 -0
  86. package/lib/types/tools.d.ts +28 -0
  87. package/lib/types/trust-fence.d.ts +24 -0
  88. package/lib/types/wire.d.ts +38 -0
  89. package/package.json +168 -0
  90. package/src/agent-pty.ts +519 -0
  91. package/src/browser-probe.ts +26 -0
  92. package/src/bundle-route.ts +130 -0
  93. package/src/client/BrowserView.tsx +253 -0
  94. package/src/client/DiffTab.tsx +110 -0
  95. package/src/client/DiffView.tsx +266 -0
  96. package/src/client/EditorHost.tsx +101 -0
  97. package/src/client/ExplorerView.tsx +283 -0
  98. package/src/client/GitView.tsx +540 -0
  99. package/src/client/OrphanedTab.tsx +27 -0
  100. package/src/client/PdfView.tsx +110 -0
  101. package/src/client/PptxView.tsx +131 -0
  102. package/src/client/SandboxStatusBar.tsx +60 -0
  103. package/src/client/SideCardSection.module.css +271 -0
  104. package/src/client/SideCardSection.tsx +426 -0
  105. package/src/client/Sidebar.tsx +861 -0
  106. package/src/client/SubagentView.module.css +547 -0
  107. package/src/client/SubagentView.tsx +866 -0
  108. package/src/client/TabBar.tsx +188 -0
  109. package/src/client/TerminalView.tsx +233 -0
  110. package/src/client/TextEditor.tsx +382 -0
  111. package/src/client/api.ts +215 -0
  112. package/src/client/binary-download.tsx +23 -0
  113. package/src/client/breakpoints.ts +51 -0
  114. package/src/client/browser.ts +119 -0
  115. package/src/client/builtins/index.ts +33 -0
  116. package/src/client/builtins/tabs.tsx +211 -0
  117. package/src/client/builtins/viewers.tsx +151 -0
  118. package/src/client/chunk-loader.ts +196 -0
  119. package/src/client/chunks/docx.tsx +8 -0
  120. package/src/client/chunks/editor.tsx +9 -0
  121. package/src/client/chunks/pptx.tsx +8 -0
  122. package/src/client/chunks/terminal.tsx +9 -0
  123. package/src/client/chunks/xlsx.tsx +9 -0
  124. package/src/client/cm-themes.ts +129 -0
  125. package/src/client/conversation-draft.ts +29 -0
  126. package/src/client/css-modules.d.ts +5 -0
  127. package/src/client/docx-view.tsx +113 -0
  128. package/src/client/editor-load.ts +92 -0
  129. package/src/client/icons.tsx +137 -0
  130. package/src/client/image-types.ts +8 -0
  131. package/src/client/ime-guard.ts +47 -0
  132. package/src/client/index.tsx +238 -0
  133. package/src/client/intercept.tsx +94 -0
  134. package/src/client/lang.ts +95 -0
  135. package/src/client/layout.css +54 -0
  136. package/src/client/lazy-chunk.tsx +89 -0
  137. package/src/client/link-intercept.ts +66 -0
  138. package/src/client/locales.ts +476 -0
  139. package/src/client/office-shared.tsx +40 -0
  140. package/src/client/office-types.ts +48 -0
  141. package/src/client/office-view.tsx +12 -0
  142. package/src/client/openpath-intercept.ts +62 -0
  143. package/src/client/paths.ts +28 -0
  144. package/src/client/pdf-types.ts +4 -0
  145. package/src/client/prefs.ts +100 -0
  146. package/src/client/produced-files.ts +87 -0
  147. package/src/client/selection-payload.ts +86 -0
  148. package/src/client/service.ts +434 -0
  149. package/src/client/sidebar.module.css +1917 -0
  150. package/src/client/split-pane.tsx +292 -0
  151. package/src/client/state.ts +1048 -0
  152. package/src/client/subagent-activity.ts +67 -0
  153. package/src/client/subagent-detect.ts +125 -0
  154. package/src/client/subagent-jobs.ts +161 -0
  155. package/src/client/theme.ts +44 -0
  156. package/src/client/xlsx-to-univer.ts +260 -0
  157. package/src/client/xlsx-view.tsx +116 -0
  158. package/src/config.ts +95 -0
  159. package/src/context-types.ts +445 -0
  160. package/src/fs-tree.ts +115 -0
  161. package/src/git.ts +236 -0
  162. package/src/html-route.ts +70 -0
  163. package/src/index.ts +897 -0
  164. package/src/invariant.ts +32 -0
  165. package/src/jobs-routes.ts +257 -0
  166. package/src/prefs-shared.ts +127 -0
  167. package/src/pty-manager.ts +199 -0
  168. package/src/tools.ts +477 -0
  169. package/src/trust-fence.ts +77 -0
  170. package/src/wire.ts +94 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Lazy chunk loader for the client bundle. The heavy preview/terminal
3
+ * libraries (Univer, docx-preview, pptx-renderer, CodeMirror, xterm — the
4
+ * office/terminal/editor stacks, tens of MB) live in separate build-time
5
+ * bundles (`lib/client-<name>.js`) fetched only on first use of the feature
6
+ * that needs them, so startup downloads/parses only the ~1MB core bundle.
7
+ *
8
+ * How a chunk script works (see tsdown.config.ts chunkBundle):
9
+ *
10
+ * globalThis.__dshChunks__ = globalThis.__dshChunks__ || {};
11
+ * globalThis.__dshChunks__["terminal"] = (require) => { ...exports };
12
+ *
13
+ * The script registers its factory on a plugin-owned global registry (NOT
14
+ * through window.__ModuleLoader__.load — the module loader's import() only
15
+ * resolves seed words, shell-own modules, registered factories, and boot
16
+ * graph rows; a chunk id is none of those, so resolution would be version-
17
+ * dependent). Materialization is plugin-owned:
18
+ *
19
+ * 1. inject <script src="/sidebar/bundle/<name>.js"> (classic same-origin
20
+ * script; the official /plugins/<id>/client.js route cannot serve
21
+ * arbitrary file names, so the plugin's own host route serves the chunks),
22
+ * 2. read the factory from the global registry,
23
+ * 3. call it with a require that resolves the platform externals through
24
+ * `__DSH_MODULES__.import(spec)` — the seed-word branch, the one part of
25
+ * the module system that is stable across versions.
26
+ *
27
+ * Caching contract (three layers, each with a failure path):
28
+ * - In-memory: one in-flight promise per chunk, memoized until
29
+ * {@link resetChunks}; a failed load removes its entry so the next call
30
+ * retries from scratch.
31
+ * - Script execution: each re-execution overwrites the global registry slot
32
+ * (assignment, never registration) — no "duplicate factory registration"
33
+ * class of errors; a failed materialization clears the cache so the retry
34
+ * re-injects and re-executes.
35
+ * - HTTP: the bundle route revalidates every request (`cache-control:
36
+ * no-cache` + ETag, 304 when unchanged), so page refreshes and HMR
37
+ * re-activations never re-download a multi-MB chunk that did not change.
38
+ *
39
+ * HMR: each plugin activation calls {@link resetChunks}, which drops the
40
+ * in-memory cache (and any test registry), so a hot-reloaded core bundle
41
+ * re-fetches and re-executes the current chunk scripts on the next lazy
42
+ * open. Chunk-only source edits still need a manual page refresh (the HMR
43
+ * poll watches only client.js).
44
+ */
45
+ export type ChunkName = 'docx' | 'xlsx' | 'pptx' | 'terminal' | 'editor'
46
+
47
+ /** The module exports a chunk factory provides (namespace-ish record). */
48
+ export type ChunkExports = Record<string, unknown>
49
+
50
+ /** A chunk factory: (require) => exports (the chunk's CJS closure shape). */
51
+ type ChunkFactory = (require: (spec: string) => unknown) => ChunkExports
52
+
53
+ /**
54
+ * The platform externals a chunk bundle may require (mirror of
55
+ * CLIENT_EXTERNALS in tsdown.config.ts — the chunk builds keep these
56
+ * external and the loader resolves them here). A superset is safe: the
57
+ * require only answers what the chunk actually asks for.
58
+ */
59
+ export const CHUNK_EXTERNALS: readonly string[] = [
60
+ 'react',
61
+ 'react/jsx-runtime',
62
+ 'react-dom',
63
+ 'react-dom/client',
64
+ 'cordis',
65
+ '@deepseek-ai/dsh-client-ui-slots',
66
+ '@deepseek-ai/dsh-client-web-react',
67
+ '@deepseek-ai/dsh-client-ui-primitives',
68
+ '@deepseek-ai/dsh-client-schema-form',
69
+ '@deepseek-ai/dsh-client-runtime/client',
70
+ ]
71
+
72
+ /** Chunk script endpoint served by the plugin host half (src/bundle-route.ts). */
73
+ const CHUNK_URL = (name: ChunkName): string => `/sidebar/bundle/${name}.js`
74
+
75
+ /** The client module system surface this loader needs (window.__DSH_MODULES__). */
76
+ interface ChunkModuleSystem {
77
+ import(specifier: string): Promise<unknown>
78
+ }
79
+
80
+ /** Resolve the shell-installed module system (set before any plugin activates). */
81
+ function moduleSystem(): ChunkModuleSystem | undefined {
82
+ return (globalThis as { __DSH_MODULES__?: ChunkModuleSystem }).__DSH_MODULES__
83
+ }
84
+
85
+ /** The plugin-owned chunk factory registry the chunk scripts populate. */
86
+ interface ChunkRegistry {
87
+ [name: string]: ChunkFactory | undefined
88
+ }
89
+
90
+ function chunkRegistry(): ChunkRegistry {
91
+ const g = globalThis as { __dshChunks__?: ChunkRegistry }
92
+ return g.__dshChunks__ ??= {}
93
+ }
94
+
95
+ /** Script-load hook; tests replace it with a stub (the default needs a real DOM + network). */
96
+ export type ChunkScriptLoader = (src: string) => Promise<void>
97
+
98
+ const defaultScriptLoader: ChunkScriptLoader = (src) => new Promise((resolve, reject) => {
99
+ const el = document.createElement('script')
100
+ el.async = true
101
+ el.src = src
102
+ el.addEventListener('load', () => {
103
+ el.remove()
104
+ resolve()
105
+ }, { once: true })
106
+ el.addEventListener('error', () => {
107
+ el.remove()
108
+ reject(new Error(`[dsh-better-sidebar] chunk script ${src} failed to load`))
109
+ }, { once: true })
110
+ document.head.append(el)
111
+ })
112
+
113
+ let scriptLoader: ChunkScriptLoader = defaultScriptLoader
114
+
115
+ /** Test hook: replace the chunk-script loader (pass null to restore the default). */
116
+ export function setChunkScriptLoaderForTests(loader: ChunkScriptLoader | null): void {
117
+ scriptLoader = loader ?? defaultScriptLoader
118
+ }
119
+
120
+ /** Test/dev hook: resolve a chunk without fetching a script (e.g. vitest). */
121
+ const testLoaders = new Map<ChunkName, () => Promise<ChunkExports>>()
122
+ export function registerChunkForTests(name: ChunkName, loader: () => Promise<ChunkExports>): void {
123
+ testLoaders.set(name, loader)
124
+ }
125
+
126
+ /** Memoized externals require, resolved once per page from the seed table. */
127
+ let externalsRequire: ((spec: string) => unknown) | undefined
128
+
129
+ async function buildExternalsRequire(modules: ChunkModuleSystem): Promise<(spec: string) => unknown> {
130
+ if (externalsRequire !== undefined) return externalsRequire
131
+ // Per-spec tolerance: a spec the running DSH version cannot resolve (e.g.
132
+ // the runtime/client exemption row) stays unresolved until a chunk
133
+ // actually requires it — only then it is a loud error.
134
+ const entries = await Promise.all(CHUNK_EXTERNALS.map(async (spec) => {
135
+ try {
136
+ return [spec, await modules.import(spec)] as const
137
+ } catch {
138
+ return [spec, undefined] as const
139
+ }
140
+ }))
141
+ const table = new Map<string, unknown>(entries)
142
+ externalsRequire = (spec: string): unknown => {
143
+ if (!table.has(spec)) {
144
+ // Single quotes: the bundle-consistency scan regexes for require("...")
145
+ // lexical calls — a double-quoted literal here would trip it.
146
+ throw new Error(`[dsh-better-sidebar] chunk require('${spec}') missed the module table`)
147
+ }
148
+ return table.get(spec)
149
+ }
150
+ return externalsRequire
151
+ }
152
+
153
+ /** In-flight/memoized chunk loads; a failure removes its entry so a retry re-fetches. */
154
+ const cache = new Map<ChunkName, Promise<ChunkExports>>()
155
+
156
+ /**
157
+ * Load (once) and materialize a lazy chunk, returning its module exports.
158
+ * Concurrent callers share one in-flight load; a failure clears the cache
159
+ * entry so the next call retries (the script re-executes and overwrites its
160
+ * global registry slot — assignments are idempotent).
161
+ * @param name - the chunk to load.
162
+ */
163
+ export function loadChunk(name: ChunkName): Promise<ChunkExports> {
164
+ const cached = cache.get(name)
165
+ if (cached !== undefined) return cached
166
+ const task = (async (): Promise<ChunkExports> => {
167
+ const test = testLoaders.get(name)
168
+ if (test !== undefined) return test()
169
+ const modules = moduleSystem()
170
+ if (modules === undefined) {
171
+ throw new Error(`[dsh-better-sidebar] chunk "${name}": client module system unavailable`)
172
+ }
173
+ await scriptLoader(CHUNK_URL(name))
174
+ const factory = chunkRegistry()[name]
175
+ if (typeof factory !== 'function') {
176
+ throw new Error(`[dsh-better-sidebar] chunk "${name}" script did not register its factory`)
177
+ }
178
+ const require = await buildExternalsRequire(modules)
179
+ return factory(require)
180
+ })()
181
+ cache.set(name, task)
182
+ void task.catch(() => { cache.delete(name) })
183
+ return task
184
+ }
185
+
186
+ /**
187
+ * Drop all chunk state for a fresh plugin activation (HMR-safe): clear the
188
+ * in-memory cache and any test-registry entries, so the next lazy open
189
+ * re-fetches and re-executes the current chunk scripts (the registry slots
190
+ * are overwritten by the re-execution — no cleanup needed).
191
+ */
192
+ export function resetChunks(): void {
193
+ cache.clear()
194
+ testLoaders.clear()
195
+ externalsRequire = undefined
196
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Lazy chunk entry: the .docx preview (docx-preview + jszip). Built as
3
+ * `lib/client-docx.js` and registered under `dsh-better-sidebar/docx` —
4
+ * fetched only when a .docx is first opened (see chunk-loader.ts and
5
+ * docs/plans/2026-08-12-lazy-chunks-design.md). Never import this module
6
+ * from the core bundle: it pulls the office library into the startup path.
7
+ */
8
+ export { DocxView } from '../docx-view.tsx'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lazy chunk entry: the code/markdown/html text editor (CodeMirror 6 + the
3
+ * language packages). Built as `lib/client-editor.js` and registered under
4
+ * `dsh-better-sidebar/editor` — fetched only when a text file is first
5
+ * opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
6
+ * Never import this module from the core bundle: it pulls CodeMirror into
7
+ * the startup path.
8
+ */
9
+ export { TextEditor } from '../TextEditor.tsx'
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Lazy chunk entry: the .pptx preview (pptx-renderer). Built as
3
+ * `lib/client-pptx.js` and registered under `dsh-better-sidebar/pptx` —
4
+ * fetched only when a .pptx is first opened (see chunk-loader.ts and
5
+ * docs/plans/2026-08-12-lazy-chunks-design.md). Never import this module
6
+ * from the core bundle: it pulls the renderer into the startup path.
7
+ */
8
+ export { PptxView } from '../PptxView.tsx'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lazy chunk entry: the interactive terminal (xterm + fit addon). Built as
3
+ * `lib/client-terminal.js` and registered under
4
+ * `dsh-better-sidebar/terminal` — fetched only when a terminal tab is first
5
+ * opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
6
+ * Never import this module from the core bundle: it pulls xterm (and its
7
+ * stylesheet) into the startup path.
8
+ */
9
+ export { TerminalView } from '../TerminalView.tsx'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lazy chunk entry: the .xlsx preview (Univer family + SheetJS, several MB).
3
+ * Built as `lib/client-xlsx.js` and registered under
4
+ * `dsh-better-sidebar/xlsx` — fetched only when a .xlsx is first opened
5
+ * (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
6
+ * Never import this module from the core bundle: it pulls Univer into the
7
+ * startup path.
8
+ */
9
+ export { XlsxView } from '../xlsx-view.tsx'
@@ -0,0 +1,129 @@
1
+ /**
2
+ * CodeMirror 6 theme pieces for the sidebar editor. The editor surface
3
+ * (background, caret, gutter) rides the DSH theme tokens so it blends with
4
+ * the panel in both schemes; only the syntax token colors need concrete
5
+ * values, and those come from the same designed palettes the app's code
6
+ * surfaces use — the one-dark family for dark, the one-light family for
7
+ * light. The scheme flip reconfigures these via a compartment (see
8
+ * TextEditor), so the document, undo history and scroll survive re-theming.
9
+ */
10
+ import { Compartment } from '@codemirror/state'
11
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
12
+ import { tags, type Tag } from '@lezer/highlight'
13
+ import { EditorView } from '@codemirror/view'
14
+
15
+ /** Token-driven surface shared by both schemes (pure CSS values). */
16
+ export const cmSurfaceTheme = EditorView.theme({
17
+ '&': {
18
+ height: '100%',
19
+ fontSize: '13px',
20
+ backgroundColor: 'transparent',
21
+ color: 'var(--dsw-alias-label-primary)',
22
+ },
23
+ '.cm-scroller': {
24
+ overflow: 'auto',
25
+ fontFamily: 'var(--ds-font-family-code)',
26
+ },
27
+ '.cm-content': {
28
+ caretColor: 'var(--dsw-alias-label-primary)',
29
+ },
30
+ '.cm-gutters': {
31
+ backgroundColor: 'transparent',
32
+ color: 'var(--dsw-alias-label-tertiary)',
33
+ border: 'none',
34
+ },
35
+ })
36
+
37
+ /** Scheme-specific surface tints (selection, active line). */
38
+ function cmSurfaceTint(dark: boolean): ReturnType<typeof EditorView.theme> {
39
+ return EditorView.theme({
40
+ '.cm-selectionBackground, .cm-focused .cm-selectionBackground, ::selection': {
41
+ backgroundColor: dark ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.12)',
42
+ },
43
+ '.cm-activeLine, .cm-activeLineGutter': {
44
+ backgroundColor: dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)',
45
+ },
46
+ })
47
+ }
48
+
49
+ /** One syntax rule: a tag (or tag set) mapped to a concrete color/style. */
50
+ interface HighlightRule {
51
+ tag: Tag | readonly Tag[]
52
+ color?: string
53
+ fontStyle?: string
54
+ }
55
+
56
+ /** one-dark syntax palette (mirrors @codemirror/theme-one-dark). */
57
+ const HIGHLIGHTS_DARK: HighlightRule[] = [
58
+ { tag: tags.comment, color: '#5c6370', fontStyle: 'italic' },
59
+ { tag: tags.keyword, color: '#c678dd' },
60
+ { tag: tags.string, color: '#98c379' },
61
+ { tag: tags.number, color: '#d19a66' },
62
+ { tag: tags.bool, color: '#d19a66' },
63
+ { tag: tags.atom, color: '#d19a66' },
64
+ { tag: tags.typeName, color: '#e5c07b' },
65
+ { tag: tags.className, color: '#e5c07b' },
66
+ { tag: tags.propertyName, color: '#e06c75' },
67
+ { tag: tags.function(tags.variableName), color: '#61afef' },
68
+ { tag: tags.variableName, color: '#e06c75' },
69
+ { tag: tags.operator, color: '#56b6c2' },
70
+ { tag: tags.tagName, color: '#e06c75' },
71
+ { tag: tags.attributeName, color: '#d19a66' },
72
+ { tag: tags.heading, color: '#e06c75', fontStyle: 'bold' },
73
+ { tag: tags.emphasis, fontStyle: 'italic' },
74
+ { tag: tags.strong, fontStyle: 'bold' },
75
+ { tag: tags.link, color: '#61afef', fontStyle: 'underline' },
76
+ { tag: tags.meta, color: '#e5c07b' },
77
+ { tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
78
+ ]
79
+
80
+ /** one-light syntax palette (the light counterpart of one-dark). */
81
+ const HIGHLIGHTS_LIGHT: HighlightRule[] = [
82
+ { tag: tags.comment, color: '#a0a1a7', fontStyle: 'italic' },
83
+ { tag: tags.keyword, color: '#a626a4' },
84
+ { tag: tags.string, color: '#50a14f' },
85
+ { tag: tags.number, color: '#986801' },
86
+ { tag: tags.bool, color: '#0184bc' },
87
+ { tag: tags.atom, color: '#0184bc' },
88
+ { tag: tags.typeName, color: '#c18401' },
89
+ { tag: tags.className, color: '#c18401' },
90
+ { tag: tags.propertyName, color: '#e45649' },
91
+ { tag: tags.function(tags.variableName), color: '#c18401' },
92
+ { tag: tags.variableName, color: '#e45649' },
93
+ { tag: tags.operator, color: '#383a42' },
94
+ { tag: tags.tagName, color: '#e45649' },
95
+ { tag: tags.attributeName, color: '#986801' },
96
+ { tag: tags.heading, color: '#e45649', fontStyle: 'bold' },
97
+ { tag: tags.emphasis, fontStyle: 'italic' },
98
+ { tag: tags.strong, fontStyle: 'bold' },
99
+ { tag: tags.link, color: '#4078f2', fontStyle: 'underline' },
100
+ { tag: tags.meta, color: '#c18401' },
101
+ { tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
102
+ ]
103
+
104
+ /** The scheme-dependent extension pair (surface tint + syntax highlight). */
105
+ function cmThemeExtensions(dark: boolean): Array<ReturnType<typeof EditorView.theme>> {
106
+ return [
107
+ cmSurfaceTint(dark),
108
+ syntaxHighlighting(HighlightStyle.define(dark ? HIGHLIGHTS_DARK : HIGHLIGHTS_LIGHT)),
109
+ ]
110
+ }
111
+
112
+ /**
113
+ * A Compartment holding the two scheme-dependent extensions. Created once
114
+ * per editor view; a scheme flip dispatches `reconfigure(dark)` on it, so
115
+ * the document, undo history, scroll and keymaps survive re-theming.
116
+ */
117
+ export class CmThemeCompartment {
118
+ private readonly compartment = new Compartment()
119
+
120
+ /** `of(...)` payload for EditorState.create. */
121
+ of(dark: boolean): ReturnType<Compartment['of']> {
122
+ return this.compartment.of(cmThemeExtensions(dark))
123
+ }
124
+
125
+ /** Reconfigure for a new scheme. */
126
+ reconfigure(dark: boolean): ReturnType<Compartment['reconfigure']> {
127
+ return this.compartment.reconfigure(cmThemeExtensions(dark))
128
+ }
129
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Append text to the current session's composer draft through the
3
+ * conversation service — the shared path behind the explorer's @-reference
4
+ * button and the viewer selection popup. The service is resolved lazily
5
+ * through `ctx.get` (the inject-free read the app's own plugins use); a
6
+ * missing service or scope degrades to a logged no-op, never a crash.
7
+ */
8
+ import type { Context, SidebarConversation } from '../context-types.ts'
9
+
10
+ /**
11
+ * Append `text` to the session's composer draft (space-separated, like the
12
+ * @-mentions). Returns false — and logs — when the conversation service or
13
+ * the session scope is unavailable.
14
+ */
15
+ export function appendToDraft(ctx: Context, sessionId: string, text: string): boolean {
16
+ try {
17
+ const actx = ctx.sessions.scope(sessionId)
18
+ if (actx === undefined) return false
19
+ const conversation = ctx.get('conversation') as SidebarConversation | undefined
20
+ if (conversation === undefined) return false
21
+ const input = conversation.input.for(actx)
22
+ const draft = input.state.getSnapshot().draft
23
+ input.setDraft(draft.trim() === '' ? text : `${draft} ${text}`)
24
+ return true
25
+ } catch (error) {
26
+ console.warn('[dsh-better-sidebar] draft insert failed:', error)
27
+ return false
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS Modules class-map declaration (tsdown compiles .module.css at build). */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The .docx preview component: renders via docx-preview (preserved
3
+ * styles/images/tables). Lives in its own module (split out of the former
4
+ * office-view.tsx) so it builds as a standalone lazy chunk
5
+ * (`lib/client-docx.js`) — the library is loaded only when a .docx is first
6
+ * opened, and never pulls in the Univer/xlsx stack.
7
+ *
8
+ * Errors degrade to the shared download-button affordance, so a corrupted /
9
+ * encrypted / oversized file always leaves the user with a way to get it.
10
+ */
11
+ import { useEffect, useRef, useState } from 'react'
12
+ import { mediaUrl } from './api.ts'
13
+ import { t } from './locales.ts'
14
+ import { BinaryFallback, type LoadState, type OfficeViewProps } from './office-shared.tsx'
15
+ import css from './sidebar.module.css'
16
+
17
+ /**
18
+ * Render a .docx file via docx-preview. The library renders into a container
19
+ * div (no canvas); images and styles are inlined. Unmounting clears the
20
+ * container's innerHTML — docx-preview has no dispose API, but tearing down
21
+ * the DOM is enough.
22
+ */
23
+ export function DocxView(props: OfficeViewProps): JSX.Element {
24
+ const { scope, path, title } = props
25
+ const viewportRef = useRef<HTMLDivElement>(null)
26
+ const wrapRef = useRef<HTMLDivElement>(null)
27
+ const [load, setLoad] = useState<LoadState>({ status: 'loading' })
28
+ const [zoom, setZoom] = useState(100)
29
+
30
+ useEffect(() => {
31
+ let cancelled = false
32
+ const container = viewportRef.current
33
+ const wrap = wrapRef.current
34
+ if (container === null || wrap === null) return
35
+ setZoom(100)
36
+ void (async () => {
37
+ try {
38
+ const response = await fetch(mediaUrl(scope, path))
39
+ if (cancelled) return
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP ${response.status}`)
42
+ }
43
+ const buf = await response.arrayBuffer()
44
+ if (cancelled) return
45
+ // docx-preview ships its own CSS through the className option; the
46
+ // wrapper div scopes its render output.
47
+ const { renderAsync } = await import('docx-preview')
48
+ await renderAsync(buf, wrap, undefined, {
49
+ className: 'docx',
50
+ inWrapper: true,
51
+ ignoreWidth: false,
52
+ ignoreHeight: false,
53
+ breakPages: true,
54
+ experimental: false,
55
+ })
56
+ if (!cancelled) setLoad({ status: 'ready' })
57
+ } catch (error) {
58
+ if (!cancelled) {
59
+ setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
60
+ }
61
+ }
62
+ })()
63
+ return () => {
64
+ cancelled = true
65
+ // Tear down the rendered DOM so a reopen starts clean.
66
+ if (wrap !== null) wrap.innerHTML = ''
67
+ }
68
+ }, [scope.sessionId, scope.cwd, path])
69
+
70
+ useEffect(() => {
71
+ const viewport = viewportRef.current
72
+ if (viewport === null) return
73
+ const onWheel = (event: WheelEvent): void => {
74
+ if (!event.altKey) return
75
+ event.preventDefault()
76
+ const delta = event.deltaY < 0 ? 10 : -10
77
+ setZoom(current => Math.max(50, Math.min(200, current + delta)))
78
+ }
79
+ viewport.addEventListener('wheel', onWheel, { passive: false })
80
+ return () => { viewport.removeEventListener('wheel', onWheel) }
81
+ }, [])
82
+
83
+ return (
84
+ <div className={css.editorDocx}>
85
+ <div className={css.editorDocxViewport} ref={viewportRef}>
86
+ {load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
87
+ {load.status === 'error' && <BinaryFallback scope={scope} path={path} message={load.message} />}
88
+ {load.status !== 'error' && (
89
+ <div
90
+ className={css.editorDocxWrap}
91
+ ref={wrapRef}
92
+ aria-label={title}
93
+ style={{ zoom: zoom / 100 }}
94
+ />
95
+ )}
96
+ </div>
97
+ <div className={css.editorDocxZoom}>
98
+ <span className={css.editorDocxZoomHint}>{t('zoomHint')}</span>
99
+ <input
100
+ className={css.editorDocxZoomRange}
101
+ type="range"
102
+ min={50}
103
+ max={200}
104
+ step={10}
105
+ value={zoom}
106
+ aria-label={t('zoom')}
107
+ onChange={(event) => { setZoom(Number(event.currentTarget.value)) }}
108
+ />
109
+ <span className={css.editorDocxZoomValue}>{zoom}%</span>
110
+ </div>
111
+ </div>
112
+ )
113
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Pure editor-load planning: the decision logic the editor host needs to
3
+ * turn a matched file viewer + a host fs.read result into a render action.
4
+ * Kept dependency-free (no React, no fetch) so the strategy dispatch is
5
+ * unit-testable and the wire contract (head bytes, binary flag) is pinned.
6
+ *
7
+ * The host flow this module drives:
8
+ * 1. `matchFileViewer(path)` picks a viewer by extension/priority.
9
+ * 2. `planFirstMatch` dispatches its fetchStrategy.
10
+ * 3. An fsRead viewer fetches through the host; `planFsReadOutcome`
11
+ * decides what to do with the result — including the head-based
12
+ * re-match that lets a `detect` viewer claim a binary the extension
13
+ * match could not see (the builtin NUL probe on `binary-download`).
14
+ */
15
+ import type { FileViewerDescriptor } from './service.ts'
16
+
17
+ /** One host fs.read result (mirror of the wire; `head` present when binary). */
18
+ export interface FsReadOutcome {
19
+ binary: boolean
20
+ content: string
21
+ truncated: boolean
22
+ /** base64 of the first bytes (present on binary reads; sniffing material). */
23
+ head?: string
24
+ }
25
+
26
+ /** What the editor host should do next. */
27
+ export type EditorLoadAction =
28
+ /** No renderer: show the download UI. */
29
+ | { kind: 'binary' }
30
+ /** Render `viewer`'s component with the carried payload. */
31
+ | { kind: 'render'; viewer: FileViewerDescriptor; content?: string; truncated?: boolean; mediaUrl?: string; customData?: unknown }
32
+ /** Fetch the file through the host (fsRead strategy). */
33
+ | { kind: 'fetchFsRead'; viewer: FileViewerDescriptor }
34
+ /** Call the viewer's load() and render with its return value. */
35
+ | { kind: 'customLoad'; viewer: FileViewerDescriptor }
36
+
37
+ /** Decode the host's base64 head bytes into the sniffing buffer. */
38
+ export function decodeHead(headBase64: string): Uint8Array {
39
+ const binary = atob(headBase64)
40
+ const bytes = new Uint8Array(binary.length)
41
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i)
42
+ return bytes
43
+ }
44
+
45
+ /**
46
+ * Dispatch one matched viewer's fetchStrategy. A missing viewer or a
47
+ * `binary-download` strategy both mean "no client-side renderer" → the
48
+ * download UI. `mediaUrlOf` builds the media URL for `mediaUrl`/`none`
49
+ * strategies (pure, but scope-bound — injected by the host).
50
+ */
51
+ export function planFirstMatch(
52
+ viewer: FileViewerDescriptor | undefined,
53
+ mediaUrlOf: () => string,
54
+ ): EditorLoadAction {
55
+ if (viewer === undefined || viewer.fetchStrategy === 'binary-download') return { kind: 'binary' }
56
+ switch (viewer.fetchStrategy) {
57
+ case 'mediaUrl':
58
+ case 'none':
59
+ return { kind: 'render', viewer, mediaUrl: mediaUrlOf() }
60
+ case 'custom':
61
+ return { kind: 'customLoad', viewer }
62
+ case 'fsRead':
63
+ return { kind: 'fetchFsRead', viewer }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Decide what an fsRead result means for the editor.
69
+ * - Text: the first match stands (content is valid for any fsRead viewer).
70
+ * - Binary: the host head bytes enable a re-match — a `detect` viewer (e.g.
71
+ * a plugin sniffing a binary format) may claim the file. `custom` viewers
72
+ * load their own bytes; `mediaUrl`/`none` viewers render the media route;
73
+ * an fsRead viewer or nothing cannot render binary → download UI.
74
+ */
75
+ export function planFsReadOutcome(
76
+ viewer: FileViewerDescriptor,
77
+ result: FsReadOutcome,
78
+ rematch: (head: Uint8Array) => FileViewerDescriptor | undefined,
79
+ mediaUrlOf: () => string,
80
+ ): EditorLoadAction {
81
+ if (!result.binary) {
82
+ return { kind: 'render', viewer, content: result.content, truncated: result.truncated }
83
+ }
84
+ const claimed = result.head === undefined ? undefined : rematch(decodeHead(result.head))
85
+ if (claimed !== undefined && claimed.fetchStrategy === 'custom') {
86
+ return { kind: 'customLoad', viewer: claimed }
87
+ }
88
+ if (claimed !== undefined && (claimed.fetchStrategy === 'mediaUrl' || claimed.fetchStrategy === 'none')) {
89
+ return { kind: 'render', viewer: claimed, mediaUrl: mediaUrlOf() }
90
+ }
91
+ return { kind: 'binary' }
92
+ }