@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,135 @@
1
+ /**
2
+ * The editor tab's custom settings panel ("打开方式"): the file tree's
3
+ * "open with" configuration — the optional SSH host marking the workspace as
4
+ * remote, and the user-defined editors (name + URL template with `{path}` +
5
+ * whether they speak the VSCode URL dialect). Persisted as the editor
6
+ * blob's `openWith` key through the settings popup's `updatePluginSetting`.
7
+ *
8
+ * The popup renders the declarative rows (the editorExplorer picker) ABOVE
9
+ * this panel — SettingsBody renders the custom panel after the row list, so
10
+ * this component owns only its own section.
11
+ */
12
+ import { useState } from 'react'
13
+ import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
14
+ import {
15
+ isValidCustomEditor,
16
+ newCustomEditorId,
17
+ parseOpenWithConfig,
18
+ type CustomEditor,
19
+ type OpenWithConfig,
20
+ } from './open-with.ts'
21
+ import { t } from './locales.ts'
22
+ import css from './SideCardSection.module.css'
23
+
24
+ export function OpenWithSettings(props: {
25
+ pluginSettings: Record<string, unknown>
26
+ updatePluginSetting: (key: string, value: unknown) => void
27
+ }) {
28
+ const { pluginSettings, updatePluginSetting } = props
29
+ // Local draft seeded from the persisted blob (each commit re-renders the
30
+ // popup with the round-tripped prefs; the local state stays authoritative
31
+ // while the popup is open, so typing never fights an in-flight write).
32
+ const [draft, setDraft] = useState<OpenWithConfig>(() => parseOpenWithConfig(pluginSettings.openWith))
33
+
34
+ const commit = (next: OpenWithConfig): void => {
35
+ setDraft(next)
36
+ updatePluginSetting('openWith', next)
37
+ }
38
+
39
+ const setSshHost = (sshHost: string): void => commit({ ...draft, sshHost })
40
+
41
+ const patchCustom = (id: string, patch: Partial<CustomEditor>): void => {
42
+ commit({
43
+ ...draft,
44
+ customEditors: draft.customEditors.map(editor => editor.id === id ? { ...editor, ...patch } : editor),
45
+ })
46
+ }
47
+
48
+ const removeCustom = (id: string): void => {
49
+ commit({
50
+ ...draft,
51
+ customEditors: draft.customEditors.filter(editor => editor.id !== id),
52
+ // A removed editor can never stay pinned (the menu prunes unknown ids
53
+ // anyway; this keeps the blob clean).
54
+ pinned: draft.pinned.filter(pinnedId => pinnedId !== `custom:${id}`),
55
+ })
56
+ }
57
+
58
+ const addCustom = (): void => {
59
+ commit({
60
+ ...draft,
61
+ customEditors: [...draft.customEditors, { id: newCustomEditorId(), name: '', urlTemplate: '', isVscodeFamily: false }],
62
+ })
63
+ }
64
+
65
+ const hasInvalid = draft.customEditors.some(editor => !isValidCustomEditor(editor))
66
+
67
+ return (
68
+ <div className={css.popupRows}>
69
+ <div className={css.popupRow}>
70
+ <span className={css.rowText}>
71
+ <span className={css.title}>{t('openWithSettingsSshTitle')}</span>
72
+ <span className={css.desc}>{t('openWithSettingsSshDesc')}</span>
73
+ </span>
74
+ <input
75
+ className={css.typedInput}
76
+ value={draft.sshHost}
77
+ placeholder={t('openWithSettingsSshPlaceholder')}
78
+ spellCheck={false}
79
+ onChange={(event) => { setSshHost(event.target.value) }}
80
+ />
81
+ </div>
82
+ <div className={css.popupRow}>
83
+ <span className={css.rowText}>
84
+ <span className={css.title}>{t('openWithSettingsCustomTitle')}</span>
85
+ <span className={css.desc}>{t('openWithSettingsCustomDesc')}</span>
86
+ </span>
87
+ <span className={css.control}>
88
+ <button type="button" className={css.done} onClick={addCustom}>
89
+ {t('openWithSettingsAdd')}
90
+ </button>
91
+ </span>
92
+ </div>
93
+ {draft.customEditors.map(editor => (
94
+ <div key={editor.id} className={css.openWithEditorRow}>
95
+ <input
96
+ className={css.openWithEditorInput}
97
+ value={editor.name}
98
+ placeholder={t('openWithSettingsName')}
99
+ spellCheck={false}
100
+ onChange={(event) => { patchCustom(editor.id, { name: event.target.value }) }}
101
+ />
102
+ <input
103
+ className={css.openWithEditorTemplate}
104
+ value={editor.urlTemplate}
105
+ placeholder={t('openWithSettingsTemplate')}
106
+ spellCheck={false}
107
+ onChange={(event) => { patchCustom(editor.id, { urlTemplate: event.target.value }) }}
108
+ />
109
+ <label className={css.openWithFamily} title={t('openWithSettingsFamilyDesc')}>
110
+ <input
111
+ type="checkbox"
112
+ checked={editor.isVscodeFamily}
113
+ onChange={(event) => { patchCustom(editor.id, { isVscodeFamily: event.currentTarget.checked }) }}
114
+ />
115
+ <span>{t('openWithSettingsFamily')}</span>
116
+ </label>
117
+ <button
118
+ type="button"
119
+ className={css.openWithRemove}
120
+ aria-label={t('openWithSettingsRemove')}
121
+ title={t('openWithSettingsRemove')}
122
+ onClick={() => { removeCustom(editor.id) }}
123
+ >
124
+ <IconCloseOutline16 size={14} />
125
+ </button>
126
+ </div>
127
+ ))}
128
+ {hasInvalid && (
129
+ <div className={css.openWithHint} role="note">
130
+ {t('openWithSettingsInvalidHint')}
131
+ </div>
132
+ )}
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The file tree's "open with" vocabulary: which external editors / file
3
+ * managers the context menu can hand a row's path to, how the per-user
4
+ * configuration (custom editors, SSH host, pinned ids) is parsed out of the
5
+ * persisted blob, and how the external URL is built (local `file` URLs and
6
+ * VSCode-family SSH-remote URLs).
7
+ *
8
+ * Pure by design (no React / no `api`), so the whole surface is unit-testable
9
+ * without a DOM or a host route. The menu and the settings panel only consume
10
+ * the exported types and functions.
11
+ */
12
+ import type { CopyKey } from './locales.ts'
13
+
14
+ /** One user-configured editor (persisted in `pluginSettings['editor'].openWith`). */
15
+ export interface CustomEditor {
16
+ id: string
17
+ /** Human label shown in the menu (e.g. "Windsurf"). */
18
+ name: string
19
+ /** URL template with a `{path}` placeholder (e.g. `cursor://file/{path}`). */
20
+ urlTemplate: string
21
+ /** Whether the editor speaks the VSCode URL dialect — the only editors
22
+ * with an SSH-remote form (`<scheme>://vscode-remote/ssh-remote+<host>/…`). */
23
+ isVscodeFamily: boolean
24
+ }
25
+
26
+ /** The per-user "open with" configuration (one key of the editor blob). */
27
+ export interface OpenWithConfig {
28
+ /** SSH host (user@host or an ~/.ssh/config alias); '' = local workspace. */
29
+ sshHost: string
30
+ /** User-defined editors, appended after the built-ins. */
31
+ customEditors: CustomEditor[]
32
+ /** Ids of open-with targets pinned to the menu's top level
33
+ * ('explorer' | 'vscode' | 'cursor' | 'zed' | 'custom:<id>'). */
34
+ pinned: string[]
35
+ }
36
+
37
+ /** One menu-visible open target (built-in or custom, SSH-filtered). */
38
+ export interface OpenWithTarget {
39
+ /** Stable id (`explorer` / `vscode` / `cursor` / `zed` / `custom:<id>`). */
40
+ id: string
41
+ /** Locale key of a built-in label (custom editors carry `name` instead). */
42
+ nameKey?: CopyKey
43
+ /** User-defined label (custom editors only; '' for built-ins). */
44
+ name: string
45
+ /** 'reveal' = show in the OS file manager; 'url' = open a URL. */
46
+ kind: 'reveal' | 'url'
47
+ /** URL template with `{path}`; undefined for reveal targets. */
48
+ urlTemplate?: string
49
+ /** Whether the editor talks the VSCode URL dialect. */
50
+ isVscodeFamily: boolean
51
+ /** Hidden in SSH mode (a host-local opener cannot reach a remote path). */
52
+ localOnly: boolean
53
+ }
54
+
55
+ /** The default open-with configuration (fresh documents). */
56
+ export const OPEN_WITH_DEFAULTS: OpenWithConfig = {
57
+ sshHost: '',
58
+ customEditors: [],
59
+ pinned: [],
60
+ }
61
+
62
+ /** The built-in open targets, in menu order. */
63
+ export const OPEN_WITH_BUILTINS: readonly OpenWithTarget[] = [
64
+ {
65
+ id: 'explorer',
66
+ nameKey: 'openWithExplorer',
67
+ name: '',
68
+ kind: 'reveal',
69
+ isVscodeFamily: false,
70
+ localOnly: true,
71
+ },
72
+ {
73
+ id: 'vscode',
74
+ nameKey: 'openWithVscode',
75
+ name: '',
76
+ kind: 'url',
77
+ urlTemplate: 'vscode://file/{path}',
78
+ isVscodeFamily: true,
79
+ localOnly: false,
80
+ },
81
+ {
82
+ id: 'cursor',
83
+ nameKey: 'openWithCursor',
84
+ name: '',
85
+ kind: 'url',
86
+ urlTemplate: 'cursor://file/{path}',
87
+ isVscodeFamily: true,
88
+ localOnly: false,
89
+ },
90
+ {
91
+ id: 'zed',
92
+ nameKey: 'openWithZed',
93
+ name: '',
94
+ kind: 'url',
95
+ urlTemplate: 'zed://file/{path}',
96
+ isVscodeFamily: false,
97
+ localOnly: true,
98
+ },
99
+ ]
100
+
101
+ /** Whether a persisted value makes a structurally valid custom-editor row.
102
+ * Name/template may be empty — the settings panel edits rows in place and
103
+ * an in-progress row must survive the round-trip; the MENU hides rows that
104
+ * fail the stricter {@link isValidCustomEditor} check. */
105
+ function isCustomEditor(value: unknown): value is CustomEditor {
106
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
107
+ const record = value as Record<string, unknown>
108
+ return typeof record.id === 'string' && record.id !== ''
109
+ && typeof record.name === 'string'
110
+ && typeof record.urlTemplate === 'string'
111
+ && typeof record.isVscodeFamily === 'boolean'
112
+ }
113
+
114
+ /**
115
+ * Parse the persisted `openWith` blob (tolerant): malformed fields fall back
116
+ * to the defaults, malformed custom-editor rows are dropped, and pinned ids
117
+ * are kept verbatim (unknown ids are pruned when the targets are resolved —
118
+ * the menu is the only consumer of the resolved list).
119
+ */
120
+ export function parseOpenWithConfig(raw: unknown): OpenWithConfig {
121
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return { ...OPEN_WITH_DEFAULTS }
122
+ const record = raw as Record<string, unknown>
123
+ const sshHost = typeof record.sshHost === 'string' ? record.sshHost : ''
124
+ const customEditors = Array.isArray(record.customEditors)
125
+ ? record.customEditors.filter(isCustomEditor)
126
+ : []
127
+ const pinned = Array.isArray(record.pinned)
128
+ ? record.pinned.filter((id): id is string => typeof id === 'string' && id !== '')
129
+ : []
130
+ return { sshHost, customEditors, pinned }
131
+ }
132
+
133
+ /** Whether a custom editor id belongs to this config (id prefix match). */
134
+ function customIdOf(id: string): string {
135
+ return `custom:${id}`
136
+ }
137
+
138
+ /**
139
+ * The menu-visible open targets, in order (built-ins then custom editors).
140
+ * In SSH mode the local-only targets (the OS file manager, Zed, custom
141
+ * editors without the VSCode dialect) are dropped — they cannot reach a
142
+ * remote path. Unknown pinned ids are pruned here too.
143
+ */
144
+ export function resolveOpenWithTargets(config: OpenWithConfig): OpenWithTarget[] {
145
+ const ssh = config.sshHost.trim() !== ''
146
+ const targets: OpenWithTarget[] = [
147
+ ...OPEN_WITH_BUILTINS,
148
+ ...config.customEditors
149
+ .filter(isValidCustomEditor)
150
+ .map((editor): OpenWithTarget => ({
151
+ id: customIdOf(editor.id),
152
+ name: editor.name,
153
+ kind: 'url',
154
+ urlTemplate: editor.urlTemplate,
155
+ isVscodeFamily: editor.isVscodeFamily,
156
+ localOnly: !editor.isVscodeFamily,
157
+ })),
158
+ ]
159
+ return targets.filter(target => !(ssh && target.localOnly))
160
+ }
161
+
162
+ /** The SSH hint appended to a target's label in remote mode. */
163
+ export function openWithSshActive(config: OpenWithConfig): boolean {
164
+ return config.sshHost.trim() !== ''
165
+ }
166
+
167
+ /**
168
+ * The URL to open for one resolved target, or undefined when the target has
169
+ * no URL form (reveal) or the template is malformed. The path is inserted
170
+ * RAW into the template (browsers percent-encode as needed; VSCode-family
171
+ * URL parsers consume the absolute path with its leading slash, e.g.
172
+ * `vscode://file//home/u/f.ts` or `vscode://file/C:/Users/u/f.ts`).
173
+ */
174
+ export function openWithUrl(target: OpenWithTarget, path: string, config: OpenWithConfig): string | undefined {
175
+ if (target.kind !== 'url' || target.urlTemplate === undefined) return undefined
176
+ const normalized = normalizeUrlPath(path)
177
+ const ssh = openWithSshActive(config)
178
+ if (ssh && target.isVscodeFamily) {
179
+ const scheme = schemeOf(target.urlTemplate)
180
+ if (scheme === undefined) return undefined
181
+ // `ssh-remote+<host>` owns NO slash of its own: the path keeps its
182
+ // leading slash, so `/home/u/f.ts` lands as `…+host/home/u/f.ts`.
183
+ return `${scheme}://vscode-remote/ssh-remote+${config.sshHost.trim()}${normalized}`
184
+ }
185
+ if (!target.urlTemplate.includes('{path}') || !hasUrlScheme(target.urlTemplate)) return undefined
186
+ return target.urlTemplate.replace('{path}', normalized)
187
+ }
188
+
189
+ /** Whether a template starts with a `scheme://` prefix (the only shape the
190
+ * host's external opener accepts and the settings panel suggests). */
191
+ function hasUrlScheme(template: string): boolean {
192
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(template)
193
+ }
194
+
195
+ /** The scheme of a URL template (the part before the first ':'), or undefined. */
196
+ function schemeOf(template: string): string | undefined {
197
+ const at = template.indexOf(':')
198
+ if (at <= 0) return undefined
199
+ const scheme = template.slice(0, at)
200
+ return /^[a-z][a-z0-9+.-]*$/i.test(scheme) ? scheme : undefined
201
+ }
202
+
203
+ /** Normalize a filesystem path for embedding in a URL (backslashes → '/'). */
204
+ export function normalizeUrlPath(path: string): string {
205
+ return path.replace(/\\/g, '/')
206
+ }
207
+
208
+ /** A fresh custom-editor id (uuid when available, time-based fallback). */
209
+ export function newCustomEditorId(): string {
210
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
211
+ return crypto.randomUUID()
212
+ }
213
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
214
+ }
215
+
216
+ /** Validate one custom-editor row before the settings panel accepts it. */
217
+ export function isValidCustomEditor(row: { name: string; urlTemplate: string }): boolean {
218
+ return row.name.trim() !== ''
219
+ && row.urlTemplate.includes('{path}')
220
+ && /^[a-z][a-z0-9+.-]*:\/\//i.test(row.urlTemplate.trim())
221
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Interception of the chat's file-open funnel. The client runtime's
3
+ * `ctx.workspaces.openPath` is the SINGLE door every chat-side file open goes
4
+ * through — ui-conversation's apply.ts resolves the path against the session
5
+ * cwd and calls it for tool-row path links, the produced-files row, and
6
+ * prose file mentions alike (verified against the DSH source:
7
+ * `packages/client/ui-conversation/src/client/apply.ts` is the only
8
+ * production caller). Wrapping that one method reroutes those opens into the
9
+ * sidebar editor instead of the Host OS — no DSH modification needed.
10
+ *
11
+ * The wrapper is dependency-free by design (no React / ui-primitives), so
12
+ * the takeover logic is unit-testable and the file stays importable from the
13
+ * test runtime.
14
+ */
15
+
16
+ /** The one service method the wrapper replaces (mirror of the runtime IWorkspaces). */
17
+ export interface OpenPathService {
18
+ openPath(path: string): Promise<void>
19
+ }
20
+
21
+ /** Per-call decisions the wrapper needs (wired to the store + ctx in the client half). */
22
+ export interface OpenPathInterceptDeps {
23
+ /**
24
+ * Whether to take over this call: the `interceptOpenPath` pref AND the
25
+ * editor tab's own enable switch must both be on (an editor that cannot
26
+ * open must not swallow opens — they fall through to the Host).
27
+ */
28
+ takeoverEnabled(): boolean
29
+ /** The session whose scope the sidebar editor loads the file in (current session). */
30
+ currentSessionId(): string | undefined
31
+ /** Route the open into the sidebar editor (the established openSidebarFile). */
32
+ openInSidebar(path: string, sessionId: string): void
33
+ }
34
+
35
+ /**
36
+ * Wrap `workspaces.openPath`: intercepted calls open the file in the sidebar
37
+ * editor instead of the Host OS and resolve as success (the original's
38
+ * callers ignore the result); anything that declines falls through to the
39
+ * original method untouched.
40
+ * @param workspaces - the client workspaces service to wrap.
41
+ * @param deps - per-call takeover decisions.
42
+ * @returns the disposer restoring the original method (HMR-safe).
43
+ */
44
+ export function wrapOpenPath(workspaces: OpenPathService, deps: OpenPathInterceptDeps): () => void {
45
+ // The RAW method reference (never a bound copy): restore must put back the
46
+ // exact original so a chain of wrappers (other plugins wrapping the same
47
+ // method) keeps working across disposals in any order.
48
+ const original = workspaces.openPath
49
+ workspaces.openPath = (path: string): Promise<void> => {
50
+ if (deps.takeoverEnabled()) {
51
+ const sessionId = deps.currentSessionId()
52
+ if (sessionId !== undefined) {
53
+ deps.openInSidebar(path, sessionId)
54
+ return Promise.resolve()
55
+ }
56
+ }
57
+ return original.call(workspaces, path)
58
+ }
59
+ return () => {
60
+ workspaces.openPath = original
61
+ }
62
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Path projection helpers shared by the explorer rows: a path relative to
3
+ * the session cwd (for the @-reference button and "copy relative path").
4
+ * The fs-tree joins with '/' even on Windows, so both separators normalize
5
+ * to '/' before comparison.
6
+ *
7
+ * This module is dependency-free (no node:path in the client bundle): the
8
+ * host is the authority for path semantics, so this mirror deliberately
9
+ * accepts a SUPERSET of absolute forms — anything a Windows host would emit
10
+ * (drive letters, UNC) plus POSIX roots. A form the host would reject
11
+ * (e.g. a backslash UNC path on a POSIX host) passes through here and then
12
+ * fails loudly in the host's requireAbsolute instead of being silently
13
+ * joined onto the cwd.
14
+ */
15
+
16
+ /**
17
+ * Mirror of the host's absolute-path notion (see fs-tree.requireAbsolute):
18
+ * POSIX roots, Windows drive letters, and Windows UNC network shares in
19
+ * both backslash (`\\server\share\...`) and forward-slash
20
+ * (`//server/share/...`) form. Deliberately a superset — see the module
21
+ * comment — so a produced UNC path is never joined onto the cwd.
22
+ */
23
+ export function isAbsolutePath(path: string): boolean {
24
+ return path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(path) || /^[\\/]{2}[^\\/]/.test(path)
25
+ }
26
+
27
+ /**
28
+ * The path relative to the session's working directory.
29
+ * @param cwd - the explorer root (absolute).
30
+ * @param path - an absolute entry path from the fs-tree.
31
+ * @returns the relative path with '/' separators ('.' for the cwd itself),
32
+ * or `path` unchanged when it lies outside the cwd.
33
+ *
34
+ * The prefix test is case-insensitive: Windows paths (and macOS's
35
+ * case-insensitive volumes) may arrive with different casing than the cwd
36
+ * row, and the containment decision must not depend on it. The returned
37
+ * relative text keeps the caller's own casing.
38
+ */
39
+ export function relativeTo(cwd: string, path: string): string {
40
+ const base = cwd.replace(/[\\/]+$/, '')
41
+ const norm = (value: string): string => value.replace(/\\/g, '/')
42
+ const nBase = norm(base)
43
+ const nPath = norm(path)
44
+ if (nPath === nBase) return '.'
45
+ if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1)
46
+ return path
47
+ }
@@ -0,0 +1,4 @@
1
+ /** Pure PDF extension dispatch, separate from React for unit testing. */
2
+ export function isPdfExt(ext: string): boolean {
3
+ return ext === '.pdf'
4
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pending-writes queue for the file tree's open-with config: pin toggles and
3
+ * (outside the settings popup) config edits land in the sidebar prefs as
4
+ * `pluginSettings['editor']`. Writes are serialized through one promise chain
5
+ * so a quick burst of pin clicks can never read a stale pluginSettings map
6
+ * and drop an earlier toggle; each write pushes the whole open map patch
7
+ * through the revision-free settings route and adopts the returned document.
8
+ *
9
+ * (The settings popup has its own serialized commit — SideCardSection's —
10
+ * so its rows and this helper rarely race; the shared route's last-write-wins
11
+ * semantics cover the uncommon overlap.)
12
+ */
13
+ import { api } from './api.ts'
14
+ import { parsePrefs } from './prefs.ts'
15
+ import type { SidebarStore } from './state.ts'
16
+
17
+ let queue: Promise<void> = Promise.resolve()
18
+
19
+ /**
20
+ * Merge one plugin-owned settings blob of one descriptor and persist it.
21
+ * @param store - the sidebar store (its prefs are replaced by the write result).
22
+ * @param descriptorId - the descriptor whose blob is patched ('editor' here).
23
+ * @param updater - pure patch function; receives a shallow copy of the blob.
24
+ */
25
+ export function updatePluginSettings(
26
+ store: SidebarStore,
27
+ descriptorId: string,
28
+ updater: (blob: Record<string, unknown>) => Record<string, unknown>,
29
+ ): void {
30
+ queue = queue.then(async () => {
31
+ const prefs = store.getPrefs()
32
+ const blob = prefs.pluginSettings[descriptorId] ?? {}
33
+ const next = updater({ ...blob })
34
+ const view = await api.settingsUpdate({
35
+ pluginSettings: { ...prefs.pluginSettings, [descriptorId]: next },
36
+ })
37
+ store.setPrefs(parsePrefs(view.value))
38
+ }).catch((error: unknown) => {
39
+ // The pin stays visually unchanged (no optimistic flip) and the menu
40
+ // keeps working — the write failure is logged, not surfaced.
41
+ console.error('open-with settings write failed', error)
42
+ })
43
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared vocabulary of the recommended plugin catalogs: the entry shape and
3
+ * the GitHub topic URL. The two catalogs live in sibling modules —
4
+ * `plugins-tabs.ts` (tab registrations) and `plugins-viewers.ts` (file
5
+ * previewer registrations) — and are shown in the two "add plugin" modals
6
+ * (Side card settings → the dashed cards at the end of the 侧边栏内容 /
7
+ * 文件预览 grids).
8
+ */
9
+
10
+ /** The GitHub topic page listing every repo tagged `dsh-better-sidebar`. */
11
+ export const PLUGIN_TOPIC_URL = 'https://github.com/topics/dsh-better-sidebar'
12
+
13
+ /** One curated plugin entry (name / url / description / install script). */
14
+ export interface PluginEntry {
15
+ /** Unique id (the npm package name). */
16
+ id: string
17
+ /** Short display name. */
18
+ name: string
19
+ /** GitHub repository URL. */
20
+ url: string
21
+ /** One-line description (i18n friendly: string or () => string). */
22
+ description: string | (() => string)
23
+ /** Optional catalog group heading (i18n friendly); entries without one
24
+ * render under the plain list (a flat catalog stays flat). */
25
+ category?: string | (() => string)
26
+ /** The full shell command pre-filled into the install terminal (not
27
+ * executed until the user presses Enter). */
28
+ install: string
29
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The built-in catalog of TAB-registration plugins (sidebar pages),
3
+ * shown in the "add tab plugin" modal (Side card settings → 侧边栏内容 grid
4
+ * → the dashed card). Adding an entry: append one object here (unique
5
+ * `id` = npm package name, `url` = GitHub repo, `description` =
6
+ * i18n-friendly (add a `pluginXxxDesc` key in locales.ts), `install` = the
7
+ * full one-line install script — it starts with `cd ~/.dsh` so the install
8
+ * runs with the DSH home as the working directory). Data integrity is
9
+ * guarded by `tests/plugin-list.spec.ts`.
10
+ */
11
+ import { t } from './locales.ts'
12
+ import type { PluginEntry } from './plugins-shared.ts'
13
+
14
+ /** Tab-registration plugins (alphabetical order). */
15
+ export const builtinTabPlugins: readonly PluginEntry[] = [
16
+ {
17
+ id: '@dsh-external/dsh-sentinel',
18
+ name: 'dsh-sentinel 唤醒系统',
19
+ url: 'https://github.com/fuhefei/dsh-sentinel',
20
+ description: () => t('pluginSentinelDesc'),
21
+ // The official one-line bundle-channel install (git source, build
22
+ // artifacts committed — no build step needed). The `github:…` form is
23
+ // the upstream's documented command, `cd ~/.dsh` keeps the profile
24
+ // context consistent with the other entries.
25
+ install: 'cd ~/.dsh && dsh plugin --profile web add "github:fuhefei/dsh-sentinel#v0.7.0"',
26
+ },
27
+ {
28
+ id: 'dsh-docs-panel',
29
+ name: 'dsh-docs-panel 全局文档',
30
+ url: 'https://github.com/mlosun/dsh-docs-panel',
31
+ description: () => t('pluginDocsPanelDesc'),
32
+ // dsh-docs-panel hard-depends on dsh-better-sidebar (required peer), so
33
+ // the install line installs the prerequisite first, then the plugin.
34
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add dsh-docs-panel',
35
+ },
36
+ {
37
+ id: 'dsh-flowglass',
38
+ name: 'dsh-flowglass 流镜',
39
+ url: 'https://github.com/Iwctwbh/dsh-flowglass',
40
+ description: () => t('pluginFlowglassDesc'),
41
+ // Flowglass keeps its standalone drawer as a fallback and registers the
42
+ // native tab automatically when better-sidebar is present.
43
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add dsh-flowglass',
44
+ },
45
+ {
46
+ id: 'dsh-git-forge',
47
+ name: 'dsh-git-forge Git 凭据',
48
+ url: 'https://github.com/thirsty5034/dsh-git-forge',
49
+ description: () => t('pluginGitForgeDesc'),
50
+ // Peer-depends on dsh-better-sidebar (Git Forge tab). Install the
51
+ // prerequisite first; package is GitHub-sourced until npm publish.
52
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add "dsh-git-forge@github:thirsty5034/dsh-git-forge"',
53
+ },
54
+ {
55
+ id: 'dsh-git-remotes',
56
+ name: 'dsh-git-remotes Git 远程',
57
+ url: 'https://github.com/yq04/dsh-git-remotes',
58
+ description: () => t('pluginGitRemotesDesc'),
59
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add git+https://github.com/yq04/dsh-git-remotes.git',
60
+ },
61
+ {
62
+ id: 'dsh-sidebar-qa',
63
+ name: 'dsh-sidebar-qa 划选追问',
64
+ url: 'https://github.com/ChenRuoT/dsh-sidebar-qa',
65
+ description: () => t('pluginSidebarQaDesc'),
66
+ // dsh-sidebar-qa hard-depends on dsh-better-sidebar (required peer), so
67
+ // the install line installs the prerequisite first, then the plugin.
68
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add git+https://github.com/ChenRuoT/dsh-sidebar-qa.git',
69
+ },
70
+ {
71
+ id: 'dsh-ssh-tunnel',
72
+ name: 'dsh-ssh-tunnel SSH 隧道',
73
+ url: 'https://github.com/thirsty5034/dsh-ssh-tunnel',
74
+ description: () => t('pluginSshTunnelDesc'),
75
+ // Peer-depends on dsh-better-sidebar (SSH Tunnel tab + center terminal/SFTP).
76
+ // Install the prerequisite first; package is GitHub-sourced until npm publish.
77
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add "dsh-ssh-tunnel@github:thirsty5034/dsh-ssh-tunnel"',
78
+ },
79
+ {
80
+ id: 'dsh-turn-review',
81
+ name: 'dsh-turn-review 本轮审查',
82
+ url: 'https://github.com/yq04/dsh-turn-review',
83
+ description: () => t('pluginTurnReviewDesc'),
84
+ // Needs dsh-better-sidebar (optional peer) for the tab; no model tools.
85
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-better-sidebar && dsh plugin --profile web add git+https://github.com/yq04/dsh-turn-review.git',
86
+ },
87
+ ]
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The built-in catalog of FILE-PREVIEWER plugins (file-type previewers),
3
+ * shown in the "add preview plugin" modal (Side card settings → 文件预览
4
+ * grid → the dashed card). Adding an entry: append one object here (unique
5
+ * `id` = npm package name, `url` = GitHub repo, `description` =
6
+ * i18n-friendly, `install` = the full shell command pre-filled into the
7
+ * install terminal — it starts with `cd ~/.dsh` so the install runs with
8
+ * the DSH home as the working directory). Data integrity is guarded by
9
+ * `tests/plugin-list.spec.ts`.
10
+ */
11
+ import { t } from './locales.ts'
12
+ import type { PluginEntry } from './plugins-shared.ts'
13
+
14
+ /** File-previewer plugins (alphabetical order). */
15
+ export const builtinViewerPlugins: readonly PluginEntry[] = [
16
+ {
17
+ id: '@huanlin/dsh-plugin-better-sidebar-plugin-office',
18
+ name: 'Office 预览插件',
19
+ url: 'https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office',
20
+ description: () => t('pluginOfficeDesc'),
21
+ install: 'cd ~/.dsh && dsh plugin --profile web add @huanlin/dsh-plugin-better-sidebar-plugin-office',
22
+ },
23
+ {
24
+ id: 'dsh-video-preview',
25
+ name: '视频预览插件',
26
+ url: 'https://github.com/zemul/dsh-video-preview',
27
+ description: () => t('pluginVideoPreviewDesc'),
28
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-video-preview',
29
+ },
30
+ ]