@keith9681/dsh-sidebar 0.15.2

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/README_EN.md +0 -0
  4. package/cordis.patch.yml +49 -0
  5. package/lib/client-editor.js +34651 -0
  6. package/lib/client-mermaid.js +200629 -0
  7. package/lib/client-registry.js +13388 -0
  8. package/lib/client-terminal.js +8904 -0
  9. package/lib/client.js +13388 -0
  10. package/lib/index.js +3959 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +22 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +12 -0
  19. package/lib/types/client/FileTree.d.ts +36 -0
  20. package/lib/types/client/GitView.d.ts +8 -0
  21. package/lib/types/client/OrphanedTab.d.ts +11 -0
  22. package/lib/types/client/PdfView.d.ts +6 -0
  23. package/lib/types/client/RenderBoundary.d.ts +32 -0
  24. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  25. package/lib/types/client/SideCardSection.d.ts +74 -0
  26. package/lib/types/client/SideChatView.d.ts +16 -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 +43 -0
  30. package/lib/types/client/TerminalView.d.ts +24 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/TreePanel.d.ts +23 -0
  33. package/lib/types/client/UploadOverlay.d.ts +20 -0
  34. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +268 -0
  36. package/lib/types/client/binary-download.d.ts +12 -0
  37. package/lib/types/client/breakpoints.d.ts +12 -0
  38. package/lib/types/client/browser.d.ts +50 -0
  39. package/lib/types/client/builtins/index.d.ts +18 -0
  40. package/lib/types/client/builtins/tabs.d.ts +11 -0
  41. package/lib/types/client/builtins/viewers.d.ts +3 -0
  42. package/lib/types/client/chunk-loader.d.ts +124 -0
  43. package/lib/types/client/chunks/editor.d.ts +9 -0
  44. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  45. package/lib/types/client/chunks/terminal.d.ts +9 -0
  46. package/lib/types/client/cm-themes.d.ts +24 -0
  47. package/lib/types/client/conversation-draft.d.ts +14 -0
  48. package/lib/types/client/desktop-env.d.ts +35 -0
  49. package/lib/types/client/editable-state.d.ts +27 -0
  50. package/lib/types/client/editor-load.d.ts +66 -0
  51. package/lib/types/client/frame-batcher.d.ts +24 -0
  52. package/lib/types/client/icons.d.ts +57 -0
  53. package/lib/types/client/image-types.d.ts +3 -0
  54. package/lib/types/client/ime-guard.d.ts +36 -0
  55. package/lib/types/client/index.d.ts +19 -0
  56. package/lib/types/client/intercept.d.ts +33 -0
  57. package/lib/types/client/lang.d.ts +12 -0
  58. package/lib/types/client/lazy-chunk.d.ts +25 -0
  59. package/lib/types/client/link-intercept.d.ts +42 -0
  60. package/lib/types/client/locales.d.ts +366 -0
  61. package/lib/types/client/mermaid-blocks.d.ts +41 -0
  62. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  63. package/lib/types/client/mermaid.d.ts +12 -0
  64. package/lib/types/client/open-when-sized.d.ts +20 -0
  65. package/lib/types/client/open-with-settings.d.ts +4 -0
  66. package/lib/types/client/open-with.d.ts +87 -0
  67. package/lib/types/client/openpath-intercept.d.ts +41 -0
  68. package/lib/types/client/paths.d.ts +35 -0
  69. package/lib/types/client/pdf-types.d.ts +2 -0
  70. package/lib/types/client/plugin-settings.d.ts +8 -0
  71. package/lib/types/client/plugins-shared.d.ts +27 -0
  72. package/lib/types/client/plugins-tabs.d.ts +3 -0
  73. package/lib/types/client/plugins-viewers.d.ts +3 -0
  74. package/lib/types/client/prefs.d.ts +41 -0
  75. package/lib/types/client/produced-files.d.ts +23 -0
  76. package/lib/types/client/selection-payload.d.ts +27 -0
  77. package/lib/types/client/service.d.ts +440 -0
  78. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  79. package/lib/types/client/shell-presets.d.ts +48 -0
  80. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  81. package/lib/types/client/split-pane.d.ts +30 -0
  82. package/lib/types/client/state.d.ts +359 -0
  83. package/lib/types/client/subagent-detect.d.ts +54 -0
  84. package/lib/types/client/subagent-jobs.d.ts +63 -0
  85. package/lib/types/client/tab-content-memo.d.ts +33 -0
  86. package/lib/types/client/terminal-font.d.ts +20 -0
  87. package/lib/types/client/theme.d.ts +45 -0
  88. package/lib/types/client/titlebar-strip.d.ts +23 -0
  89. package/lib/types/client/upload.d.ts +69 -0
  90. package/lib/types/client/wco.d.ts +47 -0
  91. package/lib/types/config.d.ts +64 -0
  92. package/lib/types/context-types.d.ts +603 -0
  93. package/lib/types/fs-operations.d.ts +28 -0
  94. package/lib/types/fs-search.d.ts +24 -0
  95. package/lib/types/fs-tree.d.ts +51 -0
  96. package/lib/types/git.d.ts +82 -0
  97. package/lib/types/html-route.d.ts +59 -0
  98. package/lib/types/index.d.ts +47 -0
  99. package/lib/types/invariant.d.ts +15 -0
  100. package/lib/types/jobs-routes.d.ts +45 -0
  101. package/lib/types/open-external.d.ts +24 -0
  102. package/lib/types/prefs-shared.d.ts +225 -0
  103. package/lib/types/pty-deps.d.ts +78 -0
  104. package/lib/types/pty-manager.d.ts +168 -0
  105. package/lib/types/sidechat-core.d.ts +172 -0
  106. package/lib/types/sidechat-routes.d.ts +30 -0
  107. package/lib/types/subagent-activity.d.ts +44 -0
  108. package/lib/types/subagent-live-route.d.ts +44 -0
  109. package/lib/types/tools.d.ts +31 -0
  110. package/lib/types/trust-fence.d.ts +24 -0
  111. package/lib/types/wire.d.ts +38 -0
  112. package/package.json +173 -0
  113. package/scripts/install.ps1 +289 -0
  114. package/scripts/install.sh +287 -0
  115. package/src/agent-pty.ts +527 -0
  116. package/src/browser-probe.ts +26 -0
  117. package/src/bundle-route.ts +129 -0
  118. package/src/client/BrowserView.tsx +253 -0
  119. package/src/client/DiffTab.tsx +110 -0
  120. package/src/client/DiffView.tsx +305 -0
  121. package/src/client/EditorHost.tsx +525 -0
  122. package/src/client/FileTree.tsx +653 -0
  123. package/src/client/GitView.tsx +558 -0
  124. package/src/client/OrphanedTab.tsx +27 -0
  125. package/src/client/PdfView.tsx +110 -0
  126. package/src/client/RenderBoundary.tsx +49 -0
  127. package/src/client/SandboxStatusBar.tsx +60 -0
  128. package/src/client/SideCardSection.module.css +960 -0
  129. package/src/client/SideCardSection.tsx +1152 -0
  130. package/src/client/SideChatView.module.css +437 -0
  131. package/src/client/SideChatView.tsx +644 -0
  132. package/src/client/Sidebar.tsx +1148 -0
  133. package/src/client/SubagentView.module.css +547 -0
  134. package/src/client/SubagentView.tsx +883 -0
  135. package/src/client/TabBar.tsx +305 -0
  136. package/src/client/TerminalView.tsx +383 -0
  137. package/src/client/TextEditor.tsx +500 -0
  138. package/src/client/TreePanel.tsx +273 -0
  139. package/src/client/UploadOverlay.tsx +62 -0
  140. package/src/client/add-plugin-modal.tsx +199 -0
  141. package/src/client/api.ts +335 -0
  142. package/src/client/binary-download.tsx +23 -0
  143. package/src/client/breakpoints.ts +51 -0
  144. package/src/client/browser.ts +119 -0
  145. package/src/client/builtins/index.ts +37 -0
  146. package/src/client/builtins/tabs.tsx +331 -0
  147. package/src/client/builtins/viewers.tsx +125 -0
  148. package/src/client/chunk-loader.ts +351 -0
  149. package/src/client/chunks/editor.tsx +9 -0
  150. package/src/client/chunks/mermaid.tsx +10 -0
  151. package/src/client/chunks/terminal.tsx +9 -0
  152. package/src/client/cm-themes.ts +129 -0
  153. package/src/client/conversation-draft.ts +29 -0
  154. package/src/client/css-modules.d.ts +5 -0
  155. package/src/client/desktop-env.ts +72 -0
  156. package/src/client/editable-state.ts +70 -0
  157. package/src/client/editor-load.ts +92 -0
  158. package/src/client/frame-batcher.ts +56 -0
  159. package/src/client/icons.tsx +161 -0
  160. package/src/client/image-types.ts +8 -0
  161. package/src/client/ime-guard.ts +47 -0
  162. package/src/client/index.tsx +369 -0
  163. package/src/client/intercept.tsx +110 -0
  164. package/src/client/lang.ts +103 -0
  165. package/src/client/layout.css +94 -0
  166. package/src/client/lazy-chunk.tsx +89 -0
  167. package/src/client/link-intercept.ts +73 -0
  168. package/src/client/locales.ts +734 -0
  169. package/src/client/mermaid-blocks.ts +110 -0
  170. package/src/client/mermaid-sanitize.ts +93 -0
  171. package/src/client/mermaid.tsx +401 -0
  172. package/src/client/open-when-sized.ts +43 -0
  173. package/src/client/open-with-settings.tsx +135 -0
  174. package/src/client/open-with.ts +221 -0
  175. package/src/client/openpath-intercept.ts +62 -0
  176. package/src/client/paths.ts +47 -0
  177. package/src/client/pdf-types.ts +4 -0
  178. package/src/client/plugin-settings.ts +43 -0
  179. package/src/client/plugins-shared.ts +29 -0
  180. package/src/client/plugins-tabs.ts +87 -0
  181. package/src/client/plugins-viewers.ts +30 -0
  182. package/src/client/prefs.ts +207 -0
  183. package/src/client/produced-files.ts +91 -0
  184. package/src/client/selection-payload.ts +86 -0
  185. package/src/client/service.ts +838 -0
  186. package/src/client/settings-nav-icon.ts +45 -0
  187. package/src/client/shell-presets.ts +83 -0
  188. package/src/client/sidebar.module.css +2577 -0
  189. package/src/client/sidechat-transcript.ts +321 -0
  190. package/src/client/split-pane.tsx +322 -0
  191. package/src/client/state.ts +1175 -0
  192. package/src/client/subagent-detect.ts +137 -0
  193. package/src/client/subagent-jobs.ts +161 -0
  194. package/src/client/tab-content-memo.ts +46 -0
  195. package/src/client/terminal-font.ts +28 -0
  196. package/src/client/theme.ts +110 -0
  197. package/src/client/titlebar-strip.ts +37 -0
  198. package/src/client/upload.ts +187 -0
  199. package/src/client/wco.ts +120 -0
  200. package/src/config.ts +155 -0
  201. package/src/context-types.ts +610 -0
  202. package/src/fs-operations.ts +99 -0
  203. package/src/fs-search.ts +86 -0
  204. package/src/fs-tree.ts +158 -0
  205. package/src/git.ts +244 -0
  206. package/src/html-route.ts +106 -0
  207. package/src/index.ts +1132 -0
  208. package/src/invariant.ts +32 -0
  209. package/src/jobs-routes.ts +257 -0
  210. package/src/open-external.ts +90 -0
  211. package/src/prefs-shared.ts +268 -0
  212. package/src/pty-deps.ts +240 -0
  213. package/src/pty-manager.ts +376 -0
  214. package/src/sidechat-core.ts +468 -0
  215. package/src/sidechat-routes.ts +344 -0
  216. package/src/subagent-activity.ts +91 -0
  217. package/src/subagent-live-route.ts +93 -0
  218. package/src/tools.ts +479 -0
  219. package/src/trust-fence.ts +77 -0
  220. package/src/wire.ts +100 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * The 7 built-in tab descriptors: the plugin registers its own pages
3
+ * (editor / git / subagent / sidechat / terminal / browser / diff) through
4
+ * the same {@link BetterSidebarService} external plugins use — eating its
5
+ * own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
6
+ * and mints `terminal:<uuid>` ids through `createTab`; the browser mints
7
+ * `browser:<n>` the same way (no quota). The editor IS the files window
8
+ * (the old standalone explorer merged into it).
9
+ */
10
+ import { IconBranchOutline16, IconCodeOutline16, IconFolderOpen16, IconNewChatOutline16, IconPanelLeftOutline16, IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
11
+ import type { Context } from '../../context-types.ts'
12
+ import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts'
13
+ import { t } from '../locales.ts'
14
+ import { openSidebarFile } from '../intercept.tsx'
15
+ import { EditorHost } from '../EditorHost.tsx'
16
+ import { OpenWithSettings } from '../open-with-settings.tsx'
17
+ import { lazyChunkComponent } from '../lazy-chunk.tsx'
18
+ import { GitView } from '../GitView.tsx'
19
+ import { DiffTab } from '../DiffTab.tsx'
20
+ import { SubagentView } from '../SubagentView.tsx'
21
+ import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideChatView.tsx'
22
+ import { api } from '../api.ts'
23
+ import { BrowserView } from '../BrowserView.tsx'
24
+ import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
25
+ import { TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../../prefs-shared.ts'
26
+ import type { ComponentType } from 'react'
27
+ import type { SessionScope } from '../api.ts'
28
+ import type { SidebarStore } from '../state.ts'
29
+ import type { TabDescriptor } from '../service.ts'
30
+
31
+ /**
32
+ * Lazy wrapper over the terminal view: xterm (and its stylesheet) is fetched
33
+ * only when a terminal tab is first opened (see chunk-loader.ts). The
34
+ * wrapper keeps the descriptor contract `(props) => ReactNode` — Sidebar
35
+ * calls it as a plain function.
36
+ *
37
+ * TerminalView's props are { scope, tabId, store } — `tabId` is NOT part of
38
+ * TabComponentProps (it carries `tab: SidebarTab` instead), so the
39
+ * descriptor maps it explicitly; a bare pass-through would leave tabId
40
+ * undefined and TerminalView's isAgentTabId(tabId) would crash on
41
+ * `undefined.startsWith` (regression-pinned in tests/lazy-chunk.spec.tsx).
42
+ */
43
+ const LazyTerminal = lazyChunkComponent<TerminalViewProps>(
44
+ 'terminal',
45
+ (mod) => mod.TerminalView as ComponentType<TerminalViewProps> | undefined,
46
+ )
47
+
48
+ /** The terminal view's props (mirror of TerminalView's own signature). */
49
+ interface TerminalViewProps {
50
+ scope: SessionScope
51
+ tabId: string
52
+ store: SidebarStore
53
+ }
54
+
55
+ /** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
56
+ export const TERMINAL_LIMIT = 3
57
+
58
+ /** Optional per-registration builtin behavior (currently terminal title). */
59
+ export interface BuiltinTabOptions {
60
+ /** Returns the display title for newly opened terminal tabs. */
61
+ terminalTitle?: () => string
62
+ }
63
+
64
+ /** A client-side uuid for terminal tab identity (not shown in the UI). */
65
+ function terminalUuid(): string {
66
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
67
+ return crypto.randomUUID()
68
+ }
69
+ return `t${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
70
+ }
71
+
72
+ /** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */
73
+ function uiTerminalCount(state: SidebarState): number {
74
+ return allLeaves(state.splits)
75
+ .flatMap(leaf => leaf.tabs)
76
+ .filter(tab => tab.type === 'terminal' && !isAgentTabId(tab.id)).length
77
+ }
78
+
79
+ /** The 6 built-in tab descriptors. */
80
+ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): readonly TabDescriptor[] {
81
+ return [
82
+ {
83
+ id: 'editor',
84
+ // The single files window: an editor tab with no path IS the file
85
+ // explorer (empty hint + docked tree); with a path it previews/edits
86
+ // the file. Visible in the + menu in the explorer's old slot.
87
+ title: () => t('files'),
88
+ icon: (size: number) => <IconFolderOpen16 size={size} />,
89
+ order: 10,
90
+ hidden: false,
91
+ dedupeKey: (tab) => tab.path,
92
+ // Declarative settings: the file-open behavior picker (in-place switch
93
+ // vs per-path windows) renders as an iconed select row under the
94
+ // editor card's gear in the Side card settings page; the "open with"
95
+ // configuration (SSH host + custom editors) is the custom panel BELOW
96
+ // those rows — the settings seam renders rows first, custom panel after.
97
+ settings: {
98
+ toggles: [{
99
+ key: 'editorExplorer',
100
+ type: 'select',
101
+ title: () => t('editorExplorer'),
102
+ desc: () => t('editorExplorerDesc'),
103
+ options: [
104
+ {
105
+ value: true,
106
+ icon: (size: number) => <IconPanelLeftOutline16 size={size} />,
107
+ title: () => t('editorExplorerMerged'),
108
+ desc: () => t('editorExplorerMergedDesc'),
109
+ },
110
+ {
111
+ value: false,
112
+ icon: (size: number) => <IconCodeOutline16 size={size} />,
113
+ title: () => t('editorExplorerSplit'),
114
+ desc: () => t('editorExplorerSplitDesc'),
115
+ },
116
+ ],
117
+ }],
118
+ render: ({ pluginSettings, updatePluginSetting }) => (
119
+ <OpenWithSettings pluginSettings={pluginSettings} updatePluginSetting={updatePluginSetting} />
120
+ ),
121
+ },
122
+ component: ({ ctx, store, scope, tab, expanded, onToggleDir, onReferenceFile }) => (
123
+ <EditorHost
124
+ ctx={ctx}
125
+ store={store}
126
+ scope={scope}
127
+ tab={tab}
128
+ expanded={expanded ?? []}
129
+ onToggleDir={onToggleDir ?? (() => { /* no-op */ })}
130
+ onReferenceFile={onReferenceFile ?? (() => { /* no-op */ })}
131
+ />
132
+ ),
133
+ },
134
+ {
135
+ id: 'git',
136
+ title: () => t('git'),
137
+ icon: (size: number) => <IconBranchOutline16 size={size} />,
138
+ order: 20,
139
+ single: true,
140
+ component: ({ ctx, store, scope, onOpenDiff }) => (
141
+ <GitView
142
+ scope={scope}
143
+ onOpenFile={(path) => { openSidebarFile(ctx, store, scope.sessionId, path) }}
144
+ onOpenDiff={onOpenDiff ?? (() => { /* no-op */ })}
145
+ />
146
+ ),
147
+ },
148
+ {
149
+ id: 'subagent',
150
+ title: () => t('subagent'),
151
+ icon: (size: number) => <IconThinkOutline16 size={size} />,
152
+ order: 30,
153
+ single: true,
154
+ // Declarative settings: the auto-open switches render under this row in
155
+ // the Side card settings page (the Jobs page's own related settings).
156
+ settings: {
157
+ toggles: [{
158
+ key: 'autoOpenSubagent',
159
+ title: () => t('settingsSubagentTitle'),
160
+ desc: () => t('settingsSubagentDesc'),
161
+ }, {
162
+ key: 'autoOpenJobs',
163
+ title: () => t('settingsJobsTitle'),
164
+ desc: () => t('settingsJobsDesc'),
165
+ }],
166
+ },
167
+ component: ({ ctx, scope, visible, onSubagentJump }) => (
168
+ <SubagentView
169
+ sessionId={scope.sessionId}
170
+ ctx={ctx}
171
+ active={visible}
172
+ onOpenChild={(address) => { onSubagentJump?.(address.childSessionId) }}
173
+ />
174
+ ),
175
+ },
176
+ {
177
+ id: 'sidechat',
178
+ title: () => t('sideChat'),
179
+ icon: (size: number) => <IconNewChatOutline16 size={size} />,
180
+ order: 35,
181
+ // Codex-style: EVERY side conversation is its own tab. A plain open
182
+ // mints a fresh tab flagged `autoCreate` (the view creates the EMPTY
183
+ // thread on mount); a thread switch from the header menu parks the
184
+ // target id for a deterministic `sidechat:<threadId>` reattach tab.
185
+ createTab: () => {
186
+ const threadId = consumeSidechatSeed()
187
+ if (threadId !== undefined) {
188
+ return {
189
+ tab: {
190
+ id: `sidechat:${threadId}`,
191
+ type: 'sidechat',
192
+ title: t('sideChat'),
193
+ meta: { threadId },
194
+ },
195
+ }
196
+ }
197
+ return {
198
+ tab: {
199
+ id: `sidechat:new-${crypto.randomUUID()}`,
200
+ type: 'sidechat',
201
+ title: t('sideChatUntitled'),
202
+ meta: { autoCreate: true },
203
+ },
204
+ }
205
+ },
206
+ // One tab per thread: an already-open thread focuses instead of
207
+ // duplicating; unbound fresh tabs never dedupe (each mints its own).
208
+ dedupeKey: (tab) => sidechatThreadIdOf(tab),
209
+ // Closing the tab releases the thread's live agent; the session and
210
+ // its history stay persisted (reopen from any thread's header menu).
211
+ onClose: (tab) => {
212
+ const threadId = sidechatThreadIdOf(tab)
213
+ if (threadId !== undefined) {
214
+ void api.sidechatDispose(threadId).catch(() => {})
215
+ }
216
+ },
217
+ component: ({ ctx, scope, tab, visible }) => (
218
+ <SideChatView ctx={ctx} scope={scope} tab={tab} visible={visible} />
219
+ ),
220
+ },
221
+ {
222
+ id: 'terminal',
223
+ title: () => t('terminal'),
224
+ icon: (size: number) => <IconTerminalOutline16 size={size} />,
225
+ order: 40,
226
+ available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
227
+ // Declarative settings: the model-facing terminal tools switch and
228
+ // the custom font family/size rows render under this card in the Side
229
+ // card settings page (the host gates the toolset on the tools one
230
+ // independently; the font rows apply live to every terminal).
231
+ settings: {
232
+ toggles: [{
233
+ key: 'agentTerminalTools',
234
+ title: () => t('settingsToolsTitle'),
235
+ desc: () => t('settingsToolsDesc'),
236
+ }, {
237
+ key: 'terminalShell',
238
+ type: 'text',
239
+ title: () => t('settingsShellTitle'),
240
+ desc: () => t('settingsShellDesc'),
241
+ placeholder: t('settingsShellPlaceholder'),
242
+ }, {
243
+ key: 'terminalShellArgs',
244
+ type: 'text',
245
+ title: () => t('settingsShellArgsTitle'),
246
+ desc: () => t('settingsShellArgsDesc'),
247
+ placeholder: t('settingsShellArgsPlaceholder'),
248
+ }, {
249
+ key: 'terminalFontFamily',
250
+ type: 'text',
251
+ title: () => t('settingsFontFamilyTitle'),
252
+ desc: () => t('settingsFontFamilyDesc'),
253
+ placeholder: t('settingsFontFamilyPlaceholder'),
254
+ }, {
255
+ key: 'terminalFontSize',
256
+ type: 'number',
257
+ title: () => t('settingsFontSizeTitle'),
258
+ desc: () => t('settingsFontSizeDesc'),
259
+ min: TERMINAL_FONT_SIZE_MIN,
260
+ max: TERMINAL_FONT_SIZE_MAX,
261
+ unit: 'px',
262
+ }],
263
+ },
264
+ createTab: (state) => {
265
+ const count = uiTerminalCount(state)
266
+ if (count >= TERMINAL_LIMIT) return null
267
+ return {
268
+ tab: {
269
+ id: `terminal:${terminalUuid()}`,
270
+ type: 'terminal',
271
+ title: options.terminalTitle?.() ?? t('terminal'),
272
+ },
273
+ // Keep the legacy counter advancing for compatibility with older
274
+ // persisted states; new ids no longer use it.
275
+ patch: { nextTerminal: state.nextTerminal + 1 },
276
+ }
277
+ },
278
+ component: ({ tab, scope, store }) => <LazyTerminal scope={scope} store={store} tabId={tab.id} />,
279
+ },
280
+ {
281
+ id: 'browser',
282
+ title: () => t('browser'),
283
+ icon: (size: number) => <IconGlobeOutline16 size={size} />,
284
+ order: 50,
285
+ // Declarative settings: the sandbox escape hatch, the link-takeover
286
+ // MASTER switch, and the per-protocol takeover switches (http on /
287
+ // https off by default) render under this tab's row in the Side card
288
+ // settings page (the sandbox one is warned on).
289
+ settings: {
290
+ toggles: [{
291
+ key: 'browserNoSandbox',
292
+ title: () => t('settingsBrowserSandboxTitle'),
293
+ desc: () => t('settingsBrowserSandboxDesc'),
294
+ }, {
295
+ key: 'browserInterceptLinks',
296
+ title: () => t('settingsBrowserLinksTitle'),
297
+ desc: () => t('settingsBrowserLinksDesc'),
298
+ }, {
299
+ key: 'browserInterceptHttp',
300
+ title: () => t('settingsBrowserHttpTitle'),
301
+ desc: () => t('settingsBrowserHttpDesc'),
302
+ }, {
303
+ key: 'browserInterceptHttps',
304
+ title: () => t('settingsBrowserHttpsTitle'),
305
+ desc: () => t('settingsBrowserHttpsDesc'),
306
+ }],
307
+ },
308
+ createTab: (state) => ({
309
+ tab: {
310
+ id: `browser:${state.nextBrowser}`,
311
+ type: 'browser',
312
+ title: t('browser'),
313
+ },
314
+ patch: { nextBrowser: state.nextBrowser + 1 },
315
+ }),
316
+ component: (props) => <BrowserView {...props} />,
317
+ },
318
+ {
319
+ id: 'diff',
320
+ title: () => t('git'),
321
+ icon: (size: number) => <IconDiffOutline16 size={size} />,
322
+ order: -1,
323
+ hidden: true,
324
+ dedupeKey: (tab) => tab.id,
325
+ component: ({ scope, tab }) => (
326
+ tab.diff === undefined ? null
327
+ : <DiffTab sessionId={scope.sessionId} cwd={scope.cwd} diff={tab.diff} />
328
+ ),
329
+ },
330
+ ]
331
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The 6 built-in file viewer descriptors: every preview surface is a
3
+ * registered viewer (image / pdf / markdown / html / code /
4
+ * binary-download), exactly like external plugins register theirs. Office
5
+ * previews (.docx / .xlsx / .pptx) are NOT built in anymore — they moved to
6
+ * the recommended office plugin (see plugins-viewers.ts), which registers
7
+ * the same ids through this service.
8
+ *
9
+ * The `binary-download` viewer sniffs NUL bytes via `detect` for unknown
10
+ * binaries and serves legacy doc/xls/ppt by extension; `code` is the
11
+ * catch-all (`exts: []`, lowest priority) that claims any file no other
12
+ * viewer did.
13
+ *
14
+ * The heavy viewers (the CodeMirror-backed markdown/html/code) render
15
+ * through {@link lazyChunkComponent} wrappers — their libraries are fetched
16
+ * only when such a file is first opened (see chunk-loader.ts). The
17
+ * descriptor metadata (id/exts/priority/detect) is identical either way,
18
+ * so matching semantics and external-plugin overrides are unaffected; the
19
+ * `component` wrapper keeps the descriptor contract `(props) => ReactNode`.
20
+ *
21
+ * Every viewer carries the declarative settings-surface fields — `title`
22
+ * and `icon` — so the Side card settings page can render the enable/disable
23
+ * inventory without hardcoding (eating our own dogfood).
24
+ */
25
+ import { IconCodeOutline16, IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
26
+ import { lazyChunkComponent } from '../lazy-chunk.tsx'
27
+ import { PdfView } from '../PdfView.tsx'
28
+ import { BinaryDownload } from '../binary-download.tsx'
29
+ import {
30
+ IconImageOutline16,
31
+ IconMarkdownOutline16,
32
+ IconPdfOutline16,
33
+ IconHtmlOutline16,
34
+ } from '../icons.tsx'
35
+ import type { ComponentType } from 'react'
36
+ import type { FileViewerDescriptor, FileViewerProps } from '../service.ts'
37
+ import { t } from '../locales.ts'
38
+ import css from '../sidebar.module.css'
39
+
40
+ /**
41
+ * Lazy wrapper over the chunk-resident viewer component. The `pick`
42
+ * function is module-level (stable identity — the wrapper effect depends
43
+ * on it); the cast bridges the chunk exports record to the descriptor prop
44
+ * shape (the view reads only its own subset of FileViewerProps).
45
+ */
46
+ const LazyTextEditor = lazyChunkComponent<FileViewerProps>('editor', (mod) => mod.TextEditor as ComponentType<FileViewerProps> | undefined)
47
+
48
+ /** The 6 built-in file viewer descriptors. */
49
+ export function builtinViewers(): readonly FileViewerDescriptor[] {
50
+ return [
51
+ {
52
+ id: 'image',
53
+ title: () => t('viewerImage'),
54
+ icon: (size: number) => <IconImageOutline16 size={size} />,
55
+ exts: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif'],
56
+ fetchStrategy: 'mediaUrl',
57
+ component: ({ mediaUrl: url, title }) => (
58
+ <div className={css.editorImageWrap}>
59
+ <img className={css.editorImage} src={url} alt={title} />
60
+ </div>
61
+ ),
62
+ },
63
+ {
64
+ id: 'pdf',
65
+ title: () => t('viewerPdf'),
66
+ icon: (size: number) => <IconPdfOutline16 size={size} />,
67
+ exts: ['pdf'],
68
+ fetchStrategy: 'mediaUrl',
69
+ component: ({ scope, path, title }) => (
70
+ <PdfView scope={scope} path={path} title={title} />
71
+ ),
72
+ },
73
+ {
74
+ id: 'markdown',
75
+ title: () => t('viewerMarkdown'),
76
+ icon: (size: number) => <IconMarkdownOutline16 size={size} />,
77
+ exts: ['md', 'markdown'],
78
+ fetchStrategy: 'fsRead',
79
+ component: (props) => <LazyTextEditor {...props} />,
80
+ },
81
+ {
82
+ id: 'html',
83
+ title: () => t('viewerHtml'),
84
+ icon: (size: number) => <IconHtmlOutline16 size={size} />,
85
+ exts: ['html', 'htm'],
86
+ fetchStrategy: 'fsRead',
87
+ // Declarative settings: the sandbox escape hatch and the default-unsafe
88
+ // start state render under this viewer's row in the Side card settings
89
+ // page (both warned on).
90
+ settings: {
91
+ toggles: [{
92
+ key: 'htmlViewerNoSandbox',
93
+ title: () => t('settingsHtmlSandboxTitle'),
94
+ desc: () => t('settingsHtmlSandboxDesc'),
95
+ }, {
96
+ key: 'htmlViewerDefaultUnsafe',
97
+ title: () => t('settingsHtmlDefaultUnsafeTitle'),
98
+ desc: () => t('settingsHtmlDefaultUnsafeDesc'),
99
+ }],
100
+ },
101
+ component: (props) => <LazyTextEditor {...props} />,
102
+ },
103
+ {
104
+ id: 'code',
105
+ title: () => t('viewerCode'),
106
+ icon: (size: number) => <IconCodeOutline16 size={size} />,
107
+ exts: [],
108
+ priority: -100,
109
+ fetchStrategy: 'fsRead',
110
+ component: (props) => <LazyTextEditor {...props} />,
111
+ },
112
+ {
113
+ id: 'binary-download',
114
+ title: () => t('viewerBinary'),
115
+ icon: (size: number) => <IconDownloadOutline16 size={size} />,
116
+ exts: ['doc', 'xls', 'ppt'],
117
+ priority: -50,
118
+ fetchStrategy: 'binary-download',
119
+ // NUL probe: a file whose head bytes contain a NUL is binary — claimed
120
+ // before the catch-all code viewer on the head re-match.
121
+ detect: (_path, head) => head.includes(0),
122
+ component: ({ scope, path }) => <BinaryDownload scope={scope} path={path} />,
123
+ },
124
+ ]
125
+ }