dsh-coding-sidebar 1.0.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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -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 +39 -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 +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Cross-session pinned-terminal resolution (v0.17.0+).
3
+ *
4
+ * A pinned terminal tab lives in its HOME session's state (the only
5
+ * authoritative copy) — switching sessions never copies or projects it.
6
+ * The viewer session's TabBar renders the tabs OTHER sessions have pinned
7
+ * as VIRTUAL tabs appended to the first leaf's tab list, so the user sees
8
+ * them inline with their own tabs. Clicking a virtual tab activates it
9
+ * in-place: TerminalView connects to the home session's PTY via WebSocket
10
+ * (sessionId + tab query params resolve to the home PTY on the host side),
11
+ * so the terminal renders in the current workbench without jumping sessions.
12
+ *
13
+ * Visibility rule:
14
+ *
15
+ * | pin.scope | visible when |
16
+ * |-----------|--------------|
17
+ * | `global` | any session (cwd-independent) |
18
+ * | `workspace` | `viewer.cwd === tab.pin.homeCwd` (both undefined match; viewer.cwd unknown → conservative visible) |
19
+ *
20
+ * The "viewer.cwd unknown → visible" branch is intentional: during
21
+ * hydration the session summary may carry no cwd yet, and hiding pinned
22
+ * workspace tabs on first paint would flash them away. Once the cwd
23
+ * resolves, the next store notify re-runs the resolver with the real cwd.
24
+ *
25
+ * The viewer's OWN session is excluded: its pinned tabs are already on its
26
+ * own tab strip, so rendering them again as virtual tabs would double-show.
27
+ * Tabs whose `pin` field is missing or whose `type` is not `'terminal'` are
28
+ * ignored — only terminal tabs can be pinned.
29
+ */
30
+ import type { SidebarState, SidebarTab, SplitNode } from './state.ts'
31
+
32
+ /** A pinned terminal surfaced to the viewer, paired with its home session. */
33
+ export interface PinnedTabEntry {
34
+ tab: SidebarTab
35
+ homeSessionId: string
36
+ }
37
+
38
+ /** A viewer's session identity for visibility resolution. */
39
+ export interface PinnedViewer {
40
+ sessionId: string
41
+ cwd: string | undefined
42
+ }
43
+
44
+ /** The home-session scope stored on a pinned virtual tab's meta, so
45
+ * TerminalView connects to the home session's PTY (not the viewer's). */
46
+ export interface PinnedHomeScope {
47
+ sessionId: string
48
+ cwd: string | undefined
49
+ /** The original tab id in the home session (TerminalView's `tab` param). */
50
+ tabId: string
51
+ }
52
+
53
+ const PINNED_META_KEY = '__pinnedHome'
54
+ const PINNED_VID_PREFIX = 'pinned:'
55
+
56
+ /** Whether a tab id is a pinned virtual id (prefixed). */
57
+ export function isPinnedVirtualId(tabId: string): boolean {
58
+ return tabId.startsWith(PINNED_VID_PREFIX)
59
+ }
60
+
61
+ /** Parse a pinned virtual id into its home session id and original tab id.
62
+ * Format: `pinned:<homeSessionId>:<originalTabId>` — session ids are UUIDs
63
+ * (no colons), so the first colon after the prefix delimits the session. */
64
+ export function parsePinnedVirtualId(tabId: string): { homeSessionId: string; tabId: string } {
65
+ const rest = tabId.slice(PINNED_VID_PREFIX.length)
66
+ const sep = rest.indexOf(':')
67
+ if (sep < 0) return { homeSessionId: rest, tabId: '' }
68
+ return { homeSessionId: rest.slice(0, sep), tabId: rest.slice(sep + 1) }
69
+ }
70
+
71
+ /** Extract the home scope from a pinned virtual tab's meta (undefined for
72
+ * regular tabs). */
73
+ export function getPinnedHomeScope(tab: SidebarTab): PinnedHomeScope | undefined {
74
+ const meta = tab.meta as Record<string, unknown> | undefined
75
+ return (meta?.[PINNED_META_KEY] as PinnedHomeScope | undefined) ?? undefined
76
+ }
77
+
78
+ /** Whether a tab is a pinned virtual tab (injected from another session). */
79
+ export function isPinnedVirtualTab(tab: SidebarTab): boolean {
80
+ return getPinnedHomeScope(tab) !== undefined
81
+ }
82
+
83
+ /** Create a virtual SidebarTab for a pinned entry. The virtual id is unique
84
+ * (prefixed with home session) to avoid collision with the viewer's own
85
+ * tab ids; the original id is stored in meta for TerminalView. */
86
+ export function createPinnedVirtualTab(entry: PinnedTabEntry): SidebarTab {
87
+ const { tab, homeSessionId } = entry
88
+ const home: PinnedHomeScope = {
89
+ sessionId: homeSessionId,
90
+ cwd: tab.pin?.homeCwd,
91
+ tabId: tab.id,
92
+ }
93
+ return {
94
+ ...tab,
95
+ id: PINNED_VID_PREFIX + homeSessionId + ':' + tab.id,
96
+ meta: { ...(tab.meta as Record<string, unknown> | undefined ?? {}), [PINNED_META_KEY]: home },
97
+ }
98
+ }
99
+
100
+ /** Inject pinned virtual tabs into the first leaf of a split tree, and
101
+ * override that leaf's `active` when a pinned tab is activated. Returns
102
+ * the original tree when there are no pinned tabs and no active override. */
103
+ export function injectPinnedIntoTree(
104
+ tree: SplitNode,
105
+ pinned: readonly SidebarTab[],
106
+ activePinnedId: string | null,
107
+ ): SplitNode {
108
+ if (pinned.length === 0 && activePinnedId === null) return tree
109
+ if (tree.kind === 'leaf') {
110
+ return {
111
+ ...tree,
112
+ tabs: pinned.length > 0 ? [...tree.tabs, ...pinned] : tree.tabs,
113
+ active: activePinnedId ?? tree.active,
114
+ }
115
+ }
116
+ return {
117
+ ...tree,
118
+ children: [
119
+ injectPinnedIntoTree(tree.children[0]!, pinned, activePinnedId),
120
+ ...tree.children.slice(1),
121
+ ],
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Whether a pinned tab is visible to the viewer session. Conservative on
127
+ * unknown cwd: a `workspace` pin with no `homeCwd` is visible everywhere
128
+ * (the pin was set before the home session's cwd resolved), and a viewer
129
+ * whose cwd is unknown sees every workspace pin (avoids hydration flash).
130
+ */
131
+ export function pinnedVisibleTo(tab: SidebarTab, viewer: PinnedViewer): boolean {
132
+ const pin = tab.pin
133
+ if (pin === undefined) return false
134
+ if (pin.scope === 'global') return true
135
+ // workspace scope
136
+ const home = pin.homeCwd
137
+ if (home === undefined) return true
138
+ if (viewer.cwd === undefined) return true
139
+ return viewer.cwd === home
140
+ }
141
+
142
+ /**
143
+ * Collect every pinned terminal visible to the viewer across ALL cached
144
+ * session states. Excludes the viewer's own session (those tabs are on its
145
+ * own strip). Order is stable: sessions in the cache's insertion order,
146
+ * tabs in tree order (splits → floats) within each session
147
+ * — the order tabs were opened/pinned, so the rail never reorders between
148
+ * renders.
149
+ */
150
+ export function collectPinnedTabs(
151
+ bySession: ReadonlyMap<string, SidebarState>,
152
+ viewer: PinnedViewer,
153
+ ): PinnedTabEntry[] {
154
+ const entries: PinnedTabEntry[] = []
155
+ for (const [homeSessionId, state] of bySession) {
156
+ if (homeSessionId === viewer.sessionId) continue
157
+ collectFromTree(state.splits, homeSessionId, viewer, entries)
158
+ for (const float of state.floats) {
159
+ if (float.tab.type === 'terminal' && pinnedVisibleTo(float.tab, viewer)) {
160
+ entries.push({ tab: float.tab, homeSessionId })
161
+ }
162
+ }
163
+ }
164
+ return entries
165
+ }
166
+
167
+ /** Walk one split tree depth-first, collecting visible pinned terminals. */
168
+ function collectFromTree(
169
+ node: SidebarState['splits'],
170
+ homeSessionId: string,
171
+ viewer: PinnedViewer,
172
+ out: PinnedTabEntry[],
173
+ ): void {
174
+ if (node.kind === 'leaf') {
175
+ for (const tab of node.tabs) {
176
+ if (tab.type === 'terminal' && pinnedVisibleTo(tab, viewer)) {
177
+ out.push({ tab, homeSessionId })
178
+ }
179
+ }
180
+ return
181
+ }
182
+ for (const child of node.children) collectFromTree(child, homeSessionId, viewer, out)
183
+ }
@@ -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-coding-sidebar`. */
11
+ export const PLUGIN_TOPIC_URL = 'https://github.com/topics/dsh-coding-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,96 @@
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-external/ego-browser',
29
+ name: 'ego-browser Agent 浏览器',
30
+ url: 'https://github.com/Fisfzy/ego-browser',
31
+ description: () => t('pluginEgoBrowserDesc'),
32
+ // Registers a sidebar tab for the agent browser; optional peer of
33
+ // dsh-coding-sidebar (auto-tab when present, floating bubble when not).
34
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add git+https://github.com/Fisfzy/ego-browser.git',
35
+ },
36
+ {
37
+ id: 'dsh-docs-panel',
38
+ name: 'dsh-docs-panel 全局文档',
39
+ url: 'https://github.com/mlosun/dsh-docs-panel',
40
+ description: () => t('pluginDocsPanelDesc'),
41
+ // dsh-docs-panel hard-depends on dsh-coding-sidebar (required peer), so
42
+ // the install line installs the prerequisite first, then the plugin.
43
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add dsh-docs-panel',
44
+ },
45
+ {
46
+ id: 'dsh-flowglass',
47
+ name: 'dsh-flowglass 流镜',
48
+ url: 'https://github.com/Iwctwbh/dsh-flowglass',
49
+ description: () => t('pluginFlowglassDesc'),
50
+ // Flowglass keeps its standalone drawer as a fallback and registers the
51
+ // native tab automatically when dsh-coding-sidebar is present.
52
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add dsh-flowglass',
53
+ },
54
+ {
55
+ id: 'dsh-git-forge',
56
+ name: 'dsh-git-forge Git 凭据',
57
+ url: 'https://github.com/thirsty5034/dsh-git-forge',
58
+ description: () => t('pluginGitForgeDesc'),
59
+ // Peer-depends on dsh-coding-sidebar (Git Forge tab). Install the
60
+ // prerequisite first; package is GitHub-sourced until npm publish.
61
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add "dsh-git-forge@github:thirsty5034/dsh-git-forge"',
62
+ },
63
+ {
64
+ id: 'dsh-git-remotes',
65
+ name: 'dsh-git-remotes Git 远程',
66
+ url: 'https://github.com/yq04/dsh-git-remotes',
67
+ description: () => t('pluginGitRemotesDesc'),
68
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add git+https://github.com/yq04/dsh-git-remotes.git',
69
+ },
70
+ {
71
+ id: 'dsh-sidebar-qa',
72
+ name: 'dsh-sidebar-qa 划选追问',
73
+ url: 'https://github.com/ChenRuoT/dsh-sidebar-qa',
74
+ description: () => t('pluginSidebarQaDesc'),
75
+ // dsh-sidebar-qa hard-depends on dsh-coding-sidebar (required peer), so
76
+ // the install line installs the prerequisite first, then the plugin.
77
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add git+https://github.com/ChenRuoT/dsh-sidebar-qa.git',
78
+ },
79
+ {
80
+ id: 'dsh-ssh-tunnel',
81
+ name: 'dsh-ssh-tunnel SSH 隧道',
82
+ url: 'https://github.com/thirsty5034/dsh-ssh-tunnel',
83
+ description: () => t('pluginSshTunnelDesc'),
84
+ // Peer-depends on dsh-coding-sidebar (SSH Tunnel tab + center terminal/SFTP).
85
+ // Install the prerequisite first; package is GitHub-sourced until npm publish.
86
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add "dsh-ssh-tunnel@github:thirsty5034/dsh-ssh-tunnel"',
87
+ },
88
+ {
89
+ id: 'dsh-turn-review',
90
+ name: 'dsh-turn-review 本轮审查',
91
+ url: 'https://github.com/yq04/dsh-turn-review',
92
+ description: () => t('pluginTurnReviewDesc'),
93
+ // Needs dsh-coding-sidebar (optional peer) for the tab; no model tools.
94
+ install: 'cd ~/.dsh && dsh plugin --profile web add dsh-coding-sidebar && dsh plugin --profile web add git+https://github.com/yq04/dsh-turn-review.git',
95
+ },
96
+ ]
@@ -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
+ ]
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Client-side read of the user-facing "Side card" preferences. The host owns
3
+ * the namespace through the settings seam (in-process); the DSH settings RPC
4
+ * domain only serves allowlisted namespaces to configuration clients, so the
5
+ * client reads and writes THIS namespace through the plugin's own fenced
6
+ * /sidebar routes instead (api.settingsGet/settingsUpdate). Any failure
7
+ * (route rejected, namespace absent, a field of the wrong type, a value out
8
+ * of the contract range) falls back to the schema defaults — the side card
9
+ * must keep working exactly as composed when the settings surface is missing.
10
+ */
11
+ import type { api } from './api.ts'
12
+ import {
13
+ clampTerminalFontSize,
14
+ clampTitleBarStrip,
15
+ clampWidthPercent,
16
+ SIDEBAR_PREFS_DEFAULTS,
17
+ TITLE_BAR_SCHEMES,
18
+ TITLE_BAR_STRIP_DEFAULT,
19
+ type SidebarPrefs,
20
+ type TitleBarScheme,
21
+ } from '../prefs-shared.ts'
22
+
23
+ export {
24
+ SIDEBAR_PREFS_DEFAULTS,
25
+ TITLE_BAR_SCHEMES,
26
+ TITLE_BAR_STRIP_DEFAULT,
27
+ clampTerminalFontSize,
28
+ clampTitleBarStrip,
29
+ clampWidthPercent,
30
+ }
31
+ export type { SidebarPrefs, TitleBarScheme }
32
+
33
+ /** The settings wire face the preferences need (a subset of the plugin api). */
34
+ export type SidebarSettingsClient = Pick<typeof api, 'settingsGet' | 'settingsUpdate'>
35
+
36
+ /** Validate one raw resolved value into {@link SidebarPrefs}. Used for the
37
+ * settings.get payload AND the settings.update response (both carry the
38
+ * layered resolved value); any malformed field falls back to its default.
39
+ * @param value - the raw resolved section from the settings wire.
40
+ * @returns validated prefs (always well-formed).
41
+ */
42
+ export function parsePrefs(value: unknown): SidebarPrefs {
43
+ if (value === null || typeof value !== 'object') return { ...SIDEBAR_PREFS_DEFAULTS }
44
+ const record = value as Record<string, unknown>
45
+ return {
46
+ openByDefault: typeof record.openByDefault === 'boolean'
47
+ ? record.openByDefault
48
+ : SIDEBAR_PREFS_DEFAULTS.openByDefault,
49
+ defaultWidthPercent: typeof record.defaultWidthPercent === 'number' && Number.isFinite(record.defaultWidthPercent)
50
+ ? clampWidthPercent(record.defaultWidthPercent)
51
+ : SIDEBAR_PREFS_DEFAULTS.defaultWidthPercent,
52
+ autoOpenSubagent: typeof record.autoOpenSubagent === 'boolean'
53
+ ? record.autoOpenSubagent
54
+ : SIDEBAR_PREFS_DEFAULTS.autoOpenSubagent,
55
+ autoOpenJobs: typeof record.autoOpenJobs === 'boolean'
56
+ ? record.autoOpenJobs
57
+ : SIDEBAR_PREFS_DEFAULTS.autoOpenJobs,
58
+ agentTerminalTools: typeof record.agentTerminalTools === 'boolean'
59
+ ? record.agentTerminalTools
60
+ : SIDEBAR_PREFS_DEFAULTS.agentTerminalTools,
61
+ agentOpenTools: typeof record.agentOpenTools === 'boolean'
62
+ ? record.agentOpenTools
63
+ : SIDEBAR_PREFS_DEFAULTS.agentOpenTools,
64
+ terminalFontFamily: typeof record.terminalFontFamily === 'string'
65
+ ? record.terminalFontFamily
66
+ : SIDEBAR_PREFS_DEFAULTS.terminalFontFamily,
67
+ terminalShell: typeof record.terminalShell === 'string'
68
+ ? record.terminalShell
69
+ : SIDEBAR_PREFS_DEFAULTS.terminalShell,
70
+ terminalShellArgs: typeof record.terminalShellArgs === 'string'
71
+ ? record.terminalShellArgs
72
+ : SIDEBAR_PREFS_DEFAULTS.terminalShellArgs,
73
+ terminalFontSize: typeof record.terminalFontSize === 'number' && Number.isFinite(record.terminalFontSize)
74
+ ? clampTerminalFontSize(record.terminalFontSize)
75
+ : SIDEBAR_PREFS_DEFAULTS.terminalFontSize,
76
+ interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
77
+ ? record.interceptOpenPath
78
+ : SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
79
+ editorExplorer: typeof record.editorExplorer === 'boolean'
80
+ ? record.editorExplorer
81
+ : SIDEBAR_PREFS_DEFAULTS.editorExplorer,
82
+ // The title-bar scheme (auto | web | preset | custom). The schema
83
+ // declares the field WITHOUT a default, so documents written by older
84
+ // plugin versions resolve without it — migrate from the legacy fields:
85
+ // a document that ALREADY HAS VALUES (the manual compat flag on, or a
86
+ // non-default strip px — both only reachable through the old gear
87
+ // popup) maps to the `custom` scheme so the user's numbers keep
88
+ // working; a pristine document keeps the conservative `auto` scheme.
89
+ titleBarScheme: isTitleBarScheme(record.titleBarScheme)
90
+ ? record.titleBarScheme
91
+ : (record.titleBarCompat === true || hasLegacyStripValue(record.titleBarStripPx) ? 'custom' : 'auto'),
92
+ titleBarPresetId: typeof record.titleBarPresetId === 'string'
93
+ ? record.titleBarPresetId
94
+ : SIDEBAR_PREFS_DEFAULTS.titleBarPresetId,
95
+ customCss: typeof record.customCss === 'string'
96
+ ? record.customCss
97
+ : SIDEBAR_PREFS_DEFAULTS.customCss,
98
+ titleBarCompat: typeof record.titleBarCompat === 'boolean'
99
+ ? record.titleBarCompat
100
+ : SIDEBAR_PREFS_DEFAULTS.titleBarCompat,
101
+ titleBarStripPx: typeof record.titleBarStripPx === 'number' && Number.isFinite(record.titleBarStripPx)
102
+ ? clampTitleBarStrip(record.titleBarStripPx)
103
+ : SIDEBAR_PREFS_DEFAULTS.titleBarStripPx,
104
+ htmlViewerNoSandbox: typeof record.htmlViewerNoSandbox === 'boolean'
105
+ ? record.htmlViewerNoSandbox
106
+ : SIDEBAR_PREFS_DEFAULTS.htmlViewerNoSandbox,
107
+ htmlViewerDefaultUnsafe: typeof record.htmlViewerDefaultUnsafe === 'boolean'
108
+ ? record.htmlViewerDefaultUnsafe
109
+ : SIDEBAR_PREFS_DEFAULTS.htmlViewerDefaultUnsafe,
110
+ browserNoSandbox: typeof record.browserNoSandbox === 'boolean'
111
+ ? record.browserNoSandbox
112
+ : SIDEBAR_PREFS_DEFAULTS.browserNoSandbox,
113
+ browserInterceptLinks: typeof record.browserInterceptLinks === 'boolean'
114
+ ? record.browserInterceptLinks
115
+ : SIDEBAR_PREFS_DEFAULTS.browserInterceptLinks,
116
+ browserInterceptHttp: typeof record.browserInterceptHttp === 'boolean'
117
+ ? record.browserInterceptHttp
118
+ : SIDEBAR_PREFS_DEFAULTS.browserInterceptHttp,
119
+ browserInterceptHttps: typeof record.browserInterceptHttps === 'boolean'
120
+ ? record.browserInterceptHttps
121
+ : SIDEBAR_PREFS_DEFAULTS.browserInterceptHttps,
122
+ browserAllowedLoopback: typeof record.browserAllowedLoopback === 'string'
123
+ ? record.browserAllowedLoopback
124
+ : SIDEBAR_PREFS_DEFAULTS.browserAllowedLoopback,
125
+ tabsEnabled: booleanMapOf(record.tabsEnabled),
126
+ viewersEnabled: booleanMapOf(record.viewersEnabled),
127
+ pluginSettings: pluginSettingsMapOf(record.pluginSettings),
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Validate the plugin-owned settings map (v0.12.0+): `{ descriptorId: { key:
133
+ * value } }`, nested open maps. Any non-object value (or a malformed whole)
134
+ * falls back to the empty map — the schema defaults already guard the wire
135
+ * shape, this is the client's second line.
136
+ */
137
+ function pluginSettingsMapOf(value: unknown): Record<string, Record<string, unknown>> {
138
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}
139
+ const out: Record<string, Record<string, unknown>> = {}
140
+ for (const [id, blob] of Object.entries(value as Record<string, unknown>)) {
141
+ if (blob !== null && typeof blob === 'object' && !Array.isArray(blob)) {
142
+ out[id] = blob as Record<string, unknown>
143
+ }
144
+ }
145
+ return out
146
+ }
147
+
148
+ /**
149
+ * Validate one enable-switch map (per-tab / per-viewer). Only boolean values
150
+ * survive; a non-object or a non-boolean entry falls back to the empty map /
151
+ * drops the entry — an absent key means the feature stays enabled.
152
+ */
153
+ function booleanMapOf(value: unknown): Record<string, boolean> {
154
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}
155
+ const out: Record<string, boolean> = {}
156
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
157
+ if (typeof item === 'boolean') out[key] = item
158
+ }
159
+ return out
160
+ }
161
+
162
+ /** Type guard for the title-bar scheme union (anything else falls back). */
163
+ function isTitleBarScheme(value: unknown): value is TitleBarScheme {
164
+ return typeof value === 'string' && (TITLE_BAR_SCHEMES as readonly string[]).includes(value)
165
+ }
166
+
167
+ /**
168
+ * Whether the legacy document carries an explicit strip value (only
169
+ * reachable through the old gear popup): a stored number different from the
170
+ * default counts as "the user already configured something" and migrates to
171
+ * the `custom` scheme.
172
+ */
173
+ function hasLegacyStripValue(value: unknown): boolean {
174
+ return typeof value === 'number' && Number.isFinite(value) && value !== TITLE_BAR_STRIP_DEFAULT
175
+ }
176
+
177
+ /**
178
+ * Read the resolved side card preferences through the plugin's settings route.
179
+ * @param settings - the settings wire face (the plugin api by default).
180
+ * @returns validated prefs, or the schema defaults when the route rejects,
181
+ * the namespace is absent, or a stored value violates the contract.
182
+ */
183
+ export async function loadPrefs(settings: SidebarSettingsClient): Promise<SidebarPrefs> {
184
+ try {
185
+ const view = await settings.settingsGet()
186
+ return parsePrefs(view.value)
187
+ } catch {
188
+ // Transport/fence rejection or a malformed response: keep the defaults.
189
+ return { ...SIDEBAR_PREFS_DEFAULTS }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Read the external-disable flag from the same settings route: the
195
+ * dsh-web-ui family's aionui-panel provider choice. True only when the host
196
+ * resolved `aionui-panel.rightPanel` to 'aionui-panel' — while true the
197
+ * sidebar must not mount (the two right panels are mutually exclusive). Any
198
+ * failure (route rejected, aionui absent, malformed response) reads false,
199
+ * so a missing family never hides the sidebar.
200
+ * @param settings - the settings wire face (the plugin api by default).
201
+ * @returns the external-disable flag (false on any failure).
202
+ */
203
+ export async function loadExternalDisable(settings: SidebarSettingsClient): Promise<boolean> {
204
+ try {
205
+ const view = await settings.settingsGet()
206
+ return view.externalDisable === true
207
+ } catch {
208
+ return false
209
+ }
210
+ }