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,376 @@
1
+ /**
2
+ * PTY session table for the sidebar terminals. One node-pty process per
3
+ * `${sessionId}:${tabId}` key; processes survive WebSocket disconnects
4
+ * (page refresh, tab switch) and reconnect to the same process by key.
5
+ * Output is mirrored into a bounded transcript ring (capped bytes) so a new
6
+ * connection replays history before live data. Sessions die only when the
7
+ * tab is closed or the plugin tears down.
8
+ */
9
+ import { chmodSync, existsSync } from 'node:fs'
10
+ import { dirname, join } from 'node:path'
11
+ import { createRequire } from 'node:module'
12
+ import { userInfo } from 'node:os'
13
+ import type { IPty } from 'node-pty'
14
+ import { loadRequiredNodePty, type NodePtyModule } from './pty-deps.ts'
15
+ import { SidebarError } from './wire.ts'
16
+
17
+ /** Per-terminal transcript bound (bytes kept for replay). */
18
+ const TRANSCRIPT_LIMIT = 1 << 20
19
+
20
+ /**
21
+ * Restore the executable bit pnpm strips from node-pty's prebuilt
22
+ * spawn-helper (the macOS helper that forks and sets up the pty). Without it
23
+ * every spawn fails with `posix_spawnp failed`. Idempotent; mirrors
24
+ * @deepseek-ai/dsh-terminal-bash's ensure-spawn-helper postinstall, run at
25
+ * plugin activation so link-installed deployments get the fix too.
26
+ */
27
+ export function ensureSpawnHelper(): void {
28
+ if (process.platform === 'win32') return
29
+ try {
30
+ const require = createRequire(import.meta.url)
31
+ const entry = require.resolve('node-pty')
32
+ const packageRoot = dirname(dirname(entry))
33
+ const candidates = [
34
+ join(packageRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
35
+ join(packageRoot, 'build', 'Release', 'spawn-helper'),
36
+ ]
37
+ for (const helper of candidates) {
38
+ if (existsSync(helper)) chmodSync(helper, 0o755)
39
+ }
40
+ } catch {
41
+ // Resolution or chmod failure: the terminal surfaces its own spawn error.
42
+ }
43
+ }
44
+
45
+ /** One live terminal. */
46
+ export interface SidebarPty {
47
+ /** `${sessionId}:${tabId}` registry key. */
48
+ key: string
49
+ sessionId: string
50
+ tabId: string
51
+ /** The working directory the process was SPAWNED with (a reconnect that
52
+ * resolves a different authoritative cwd respawns instead of reusing —
53
+ * the page-load hydrate race can attach the real cwd after the first
54
+ * connect, and a shell in the wrong directory must not linger). */
55
+ cwd: string
56
+ pty: IPty
57
+ /** Output accumulated since spawn (bounded; head dropped when over the limit). */
58
+ transcript: string
59
+ /** Whether the top-level process exited (transcript stays replayable). */
60
+ exited: boolean
61
+ exitCode?: number | null
62
+ }
63
+
64
+ /**
65
+ * The terminal registry. `maxPerSession` bounds concurrent processes per
66
+ * conversation (the client caps tabs at the same number).
67
+ *
68
+ * Lifecycle of a UI-tab pty when its WebSocket drops:
69
+ * - **Close frame** (`{type:'close'}`): the user closed the tab → schedule a
70
+ * 0-ms close (quota released immediately).
71
+ * - **Park frame** (`{type:'park'}`): the user switched to another
72
+ * conversation; the tab is still open in its session's persisted state but
73
+ * its view unmounted → mark the pty as parked (no auto-close countdown).
74
+ * The pty stays alive until the user switches back (a reconnecting view
75
+ * calls `open()` which clears the parked state) or the tab is later closed
76
+ * (a `{type:'close'}` frame from a fresh connection). Without `park`, a
77
+ * bare socket drop would start the reconnect-grace countdown and kill the
78
+ * shell after `reconnectGraceMs` — wrong for a session switch, where the
79
+ * user is still actively using the app, just in another conversation.
80
+ * - **Bare socket drop** (no frame): page refresh, crash, plugin teardown →
81
+ * schedule a close after `reconnectGraceMs` so a quick reconnect reattaches
82
+ * the same shell.
83
+ */
84
+ export class PtyManager {
85
+ private readonly sessions = new Map<string, SidebarPty>()
86
+ private readonly pendingCloses = new Map<string, ReturnType<typeof setTimeout>>()
87
+ /** Tabs whose view unmounted because the user switched conversations — the
88
+ * tab is still open in its session's state, so the pty must NOT enter the
89
+ * reconnect-grace countdown. Cleared by `cancelClose` (a reconnecting
90
+ * view's `open()` cancels it) or by `scheduleClose` (an explicit close
91
+ * frame still kills a parked pty). */
92
+ private readonly parked = new Set<string>()
93
+
94
+ constructor(
95
+ private readonly shell: string,
96
+ private readonly maxPerSession: number,
97
+ private readonly shellArgs: string[] = [],
98
+ /** The loaded node-pty module (injected so a broken install degrades instead of crashing the plugin). */
99
+ private readonly nodePty: NodePtyModule = loadRequiredNodePty(),
100
+ ) {}
101
+
102
+ /** All live terminal keys of one session. */
103
+ keysOf(sessionId: string): string[] {
104
+ const keys: string[] = []
105
+ for (const handle of this.sessions.values()) {
106
+ if (handle.sessionId === sessionId) keys.push(handle.key)
107
+ }
108
+ return keys
109
+ }
110
+
111
+ /**
112
+ * Open (or reuse) the terminal for a session/tab key. A handle whose
113
+ * process already exited is replaced with a fresh spawn (reconnecting a
114
+ * dead terminal must yield a live shell, not an input sink), and so is a
115
+ * live handle whose spawn cwd differs from the now-authoritative one (the
116
+ * first connect of a page load can arrive before the session hydrates, so
117
+ * it fell back to the process cwd — reconnecting with the real cwd must
118
+ * restart the shell in the right directory). Reopening also cancels any
119
+ * pending scheduled close (a reconnect within the grace window keeps the
120
+ * process alive).
121
+ * @param sessionId - conversation id.
122
+ * @param tabId - client tab id.
123
+ * @param cwd - initial working directory (the session's cwd).
124
+ * @param cols - initial terminal width.
125
+ * @param rows - initial terminal height.
126
+ * @returns the live handle.
127
+ * @throws {SidebarError} pty-error when the per-session cap is reached.
128
+ */
129
+ open(
130
+ sessionId: string,
131
+ tabId: string,
132
+ cwd: string,
133
+ cols: number,
134
+ rows: number,
135
+ shell?: string,
136
+ shellArgs?: string[],
137
+ ): SidebarPty {
138
+ const key = `${sessionId}:${tabId}`
139
+ this.cancelClose(key)
140
+ const existing = this.sessions.get(key)
141
+ if (existing !== undefined && !existing.exited && existing.cwd === cwd) return existing
142
+ if (existing !== undefined) this.close(key)
143
+ // Zombie cleanup: a session's exited handles (shell closed, tab dropped
144
+ // on an old host without the close frame) must not eat the quota.
145
+ for (const [candidate, handle] of [...this.sessions]) {
146
+ if (handle.sessionId === sessionId && handle.exited) this.close(candidate)
147
+ }
148
+ if (this.keysOf(sessionId).length >= this.maxPerSession) {
149
+ throw new SidebarError('pty-error', `terminal limit reached (${this.maxPerSession}) for this session`, 400)
150
+ }
151
+ const handle: SidebarPty = {
152
+ key,
153
+ sessionId,
154
+ tabId,
155
+ cwd,
156
+ pty: this.nodePty.spawn(shell ?? this.shell, shellSpawnArgs(shellArgs ?? this.shellArgs), {
157
+ name: 'xterm-256color',
158
+ cols: Math.max(2, Math.floor(cols)),
159
+ rows: Math.max(2, Math.floor(rows)),
160
+ cwd,
161
+ env: { ...process.env },
162
+ }),
163
+ transcript: '',
164
+ exited: false,
165
+ }
166
+ handle.pty.onData((data) => {
167
+ handle.transcript += data
168
+ if (handle.transcript.length > TRANSCRIPT_LIMIT) {
169
+ handle.transcript = handle.transcript.slice(handle.transcript.length - TRANSCRIPT_LIMIT)
170
+ }
171
+ })
172
+ handle.pty.onExit(({ exitCode }) => {
173
+ handle.exited = true
174
+ handle.exitCode = exitCode
175
+ })
176
+ this.sessions.set(key, handle)
177
+ return handle
178
+ }
179
+
180
+ /**
181
+ * Schedule the terminal's destruction after `delayMs`. A tab close sends
182
+ * delay 0 (release the quota immediately); a bare socket drop (refresh,
183
+ * crash) uses the grace period so a quick reconnect keeps the process.
184
+ * `open()` cancels any pending close. Clears the parked state — an explicit
185
+ * close frame on a parked pty (the user switched back and closed the tab)
186
+ * still kills it.
187
+ */
188
+ scheduleClose(key: string, delayMs: number): void {
189
+ const handle = this.sessions.get(key)
190
+ if (handle === undefined) return
191
+ this.cancelClose(key)
192
+ const timer = setTimeout(() => { this.close(key) }, delayMs)
193
+ this.pendingCloses.set(key, timer)
194
+ }
195
+
196
+ /**
197
+ * Park a terminal: the owning tab's view unmounted because the user
198
+ * switched to another conversation, but the tab is still open in its
199
+ * session's persisted state. Cancels any pending grace close and marks
200
+ * the pty so the host's `ws.on('close')` handler does NOT start the
201
+ * reconnect-grace countdown — the pty stays alive until the user switches
202
+ * back (a reconnecting view's `open()` clears this) or explicitly closes
203
+ * the tab (a `{type:'close'}` frame's `scheduleClose` clears this).
204
+ */
205
+ park(key: string): void {
206
+ if (this.sessions.get(key) === undefined) return
207
+ this.cancelClose(key)
208
+ this.parked.add(key)
209
+ }
210
+
211
+ /** Whether this pty was parked (its view unmounted for a session switch). */
212
+ isParked(key: string): boolean {
213
+ return this.parked.has(key)
214
+ }
215
+
216
+ /** Cancel a pending scheduled close (the terminal is being reopened).
217
+ * Also clears the parked state — a reconnecting view reattaches a parked
218
+ * pty and resumes normal lifecycle. */
219
+ cancelClose(key: string): void {
220
+ const timer = this.pendingCloses.get(key)
221
+ if (timer !== undefined) {
222
+ clearTimeout(timer)
223
+ this.pendingCloses.delete(key)
224
+ }
225
+ this.parked.delete(key)
226
+ }
227
+
228
+ /** Resolve a live handle by key, or undefined. */
229
+ get(key: string): SidebarPty | undefined {
230
+ return this.sessions.get(key)
231
+ }
232
+
233
+ /** Close a terminal and drop its state (the owning tab was closed). */
234
+ close(key: string): void {
235
+ this.cancelClose(key)
236
+ const handle = this.sessions.get(key)
237
+ if (handle === undefined) return
238
+ this.sessions.delete(key)
239
+ try {
240
+ handle.pty.kill()
241
+ } catch {
242
+ // Already exited or gone; nothing left to kill.
243
+ }
244
+ }
245
+
246
+ /** Close every terminal (plugin teardown). */
247
+ disposeAll(): void {
248
+ for (const timer of this.pendingCloses.values()) clearTimeout(timer)
249
+ this.pendingCloses.clear()
250
+ for (const key of [...this.sessions.keys()]) this.close(key)
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Inputs for {@link defaultShell} resolution. Every field is optional and
256
+ * defaults to the live process, which keeps the no-argument call sites
257
+ * working while tests (and exotic embedders) can pin the platform, the
258
+ * environment, and the existence probe independently — the Windows chain
259
+ * never executes on the ubuntu CI runners, so it is only testable through
260
+ * these injection points.
261
+ */
262
+ export interface ShellResolutionOptions {
263
+ /** Platform override (defaults to `process.platform`). */
264
+ platform?: NodeJS.Platform
265
+ /** Environment override; the resolver only reads SHELL, DSH_SIDEBAR_SHELL, PATH, ProgramW6432, ProgramFiles, LOCALAPPDATA. */
266
+ env?: NodeJS.ProcessEnv
267
+ /** Explicitly configured shell (the `shell` config field); wins over every automatic source. Empty means unset. */
268
+ explicit?: string
269
+ /** File-existence probe override (defaults to `existsSync`). */
270
+ exists?: (path: string) => boolean
271
+ }
272
+
273
+ /**
274
+ * Candidate directories that may contain a `pwsh.exe` on Windows: PATH
275
+ * entries first, then the well-known machine/user install locations
276
+ * (including preview channels and per-user MSI/portable layouts). The
277
+ * machine-scope search reads both `ProgramW6432` and `ProgramFiles` so a
278
+ * 32-bit Node process — whose `ProgramFiles` points at `(x86)` — still
279
+ * finds a 64-bit PowerShell 7 install. De-duped while preserving priority
280
+ * order.
281
+ */
282
+ function windowsPwshCandidateDirs(env: NodeJS.ProcessEnv): string[] {
283
+ const dirs: string[] = []
284
+ const pathEntries = env.PATH
285
+ if (pathEntries !== undefined) {
286
+ // The win32 branch always uses the Windows PATH separator; hardcoding it
287
+ // keeps the function testable from POSIX runners without a delimiter
288
+ // injection point.
289
+ for (const entry of pathEntries.split(';')) {
290
+ const trimmed = entry.trim()
291
+ if (trimmed !== '') dirs.push(trimmed)
292
+ }
293
+ }
294
+ for (const programFiles of [env.ProgramW6432, env.ProgramFiles]) {
295
+ if (programFiles === undefined || programFiles.trim() === '') continue
296
+ dirs.push(join(programFiles, 'PowerShell', '7'))
297
+ dirs.push(join(programFiles, 'PowerShell', '7-preview'))
298
+ }
299
+ const localAppData = env.LOCALAPPDATA
300
+ if (localAppData !== undefined && localAppData.trim() !== '') {
301
+ dirs.push(join(localAppData, 'Microsoft', 'PowerShell', '7'))
302
+ dirs.push(join(localAppData, 'Microsoft', 'PowerShell', '7-preview'))
303
+ dirs.push(join(localAppData, 'Programs', 'PowerShell', '7'))
304
+ dirs.push(join(localAppData, 'Programs', 'PowerShell', '7-preview'))
305
+ }
306
+ return [...new Set(dirs)]
307
+ }
308
+
309
+ /**
310
+ * The interactive shell for this platform, resolved like a terminal
311
+ * emulator: an explicitly configured shell (the `shell` config field) wins,
312
+ * then `$SHELL` on POSIX (deployment override), then the account's login
313
+ * shell from passwd, then `/bin/bash`. The passwd step matters because
314
+ * service managers and container inits often start dsh without `SHELL`, and
315
+ * the tab should still open the user's login shell (e.g. zsh) instead of
316
+ * silently degrading to bash.
317
+ *
318
+ * Windows previously short-circuited to `powershell.exe` (the inbox 5.1)
319
+ * before any resolution, so PowerShell 7 users always got a legacy shell
320
+ * without `??`/`?.`/ternary and with poor ANSI/UTF-8 defaults. The Windows
321
+ * chain is now: explicit shell → `DSH_SIDEBAR_SHELL` env override → first
322
+ * `pwsh.exe` found on PATH or in a known install directory → the 5.1
323
+ * fallback (machines without PowerShell 7 keep working).
324
+ */
325
+ export function defaultShell(options: ShellResolutionOptions = {}): string {
326
+ const platform = options.platform ?? process.platform
327
+ const env = options.env ?? process.env
328
+ const exists = options.exists ?? existsSync
329
+ const explicit = options.explicit
330
+ if (explicit !== undefined && explicit.trim() !== '') return explicit.trim()
331
+ if (platform === 'win32') {
332
+ const envShell = env.DSH_SIDEBAR_SHELL
333
+ if (envShell !== undefined && envShell.trim() !== '') return envShell.trim()
334
+ for (const dir of windowsPwshCandidateDirs(env)) {
335
+ const candidate = join(dir, 'pwsh.exe')
336
+ if (exists(candidate)) return candidate
337
+ }
338
+ return 'powershell.exe'
339
+ }
340
+ const envShell = env.SHELL
341
+ if (envShell !== undefined && envShell.trim() !== '') return envShell.trim()
342
+ // userInfo() throws when the uid has no passwd entry (rare chroots);
343
+ // without a login shell there is nothing better than the bash default.
344
+ try {
345
+ const loginShell = userInfo().shell
346
+ if (typeof loginShell === 'string' && loginShell.trim() !== '') return loginShell
347
+ } catch {
348
+ // no passwd entry: fall through to /bin/bash
349
+ }
350
+ return '/bin/bash'
351
+ }
352
+
353
+ /**
354
+ * A short display name for a shell executable, used as the terminal tab
355
+ * title. `/bin/zsh` → `zsh`, `C:\...\powershell.exe` → `powershell`.
356
+ * Falls back to the raw value when no basename can be derived.
357
+ */
358
+ export function shellDisplayName(shell: string): string {
359
+ const normalized = shell.replace(/\\/g, '/')
360
+ const base = normalized.slice(normalized.lastIndexOf('/') + 1)
361
+ if (base === '') return shell
362
+ return base.replace(/\.(exe|cmd|bat)$/i, '')
363
+ }
364
+
365
+ /**
366
+ * Spawn arguments that make the shell behave like a terminal-emulator tab:
367
+ * POSIX shells start as login shells (`-l`) so they read the profile files
368
+ * (`~/.profile`, `~/.zprofile`); Windows PowerShell takes no login flag.
369
+ *
370
+ * When explicit `configured` args are supplied they REPLACE the platform
371
+ * defaults entirely, giving deployments full control over shell startup.
372
+ */
373
+ export function shellSpawnArgs(configured: string[] = []): string[] {
374
+ if (configured.length > 0) return [...configured]
375
+ return process.platform === 'win32' ? [] : ['-l']
376
+ }