@chatcode/cco-better-sidebar 0.18.0-alpha.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 (329) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/README_EN.md +563 -0
  4. package/assets/logo.svg +1 -0
  5. package/cordis.patch.yml +79 -0
  6. package/lib/client-editor.js +55744 -0
  7. package/lib/client-mermaid.js +208256 -0
  8. package/lib/client-registry.js +30378 -0
  9. package/lib/client-terminal.js +16894 -0
  10. package/lib/client.js +30378 -0
  11. package/lib/index.js +6897 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-opens.d.ts +65 -0
  14. package/lib/types/agent-pty.d.ts +226 -0
  15. package/lib/types/browser-probe.d.ts +15 -0
  16. package/lib/types/bundle-route.d.ts +12 -0
  17. package/lib/types/chatcode-update.d.ts +47 -0
  18. package/lib/types/client/AccountMenu.d.ts +1 -0
  19. package/lib/types/client/AiRatioSection.d.ts +3 -0
  20. package/lib/types/client/BrowserView.d.ts +39 -0
  21. package/lib/types/client/ChatcodeGate.d.ts +6 -0
  22. package/lib/types/client/CodingPlanModelsSettings.d.ts +9 -0
  23. package/lib/types/client/DiffTab.d.ts +6 -0
  24. package/lib/types/client/DiffView.d.ts +51 -0
  25. package/lib/types/client/EditorHost.d.ts +13 -0
  26. package/lib/types/client/FenceErrorNotice.d.ts +5 -0
  27. package/lib/types/client/FileTree.d.ts +41 -0
  28. package/lib/types/client/FreeWindow.d.ts +29 -0
  29. package/lib/types/client/GitView.d.ts +12 -0
  30. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  31. package/lib/types/client/OrphanedTab.d.ts +11 -0
  32. package/lib/types/client/PdfView.d.ts +6 -0
  33. package/lib/types/client/ReleaseNotesDialog.d.ts +11 -0
  34. package/lib/types/client/RenderBoundary.d.ts +32 -0
  35. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  36. package/lib/types/client/SideCardSection.d.ts +74 -0
  37. package/lib/types/client/Sidebar.d.ts +8 -0
  38. package/lib/types/client/TabBar.d.ts +54 -0
  39. package/lib/types/client/TerminalView.d.ts +24 -0
  40. package/lib/types/client/TextEditor.d.ts +10 -0
  41. package/lib/types/client/TreePanel.d.ts +27 -0
  42. package/lib/types/client/UpdateDialog.d.ts +10 -0
  43. package/lib/types/client/UpdateSettingsPanel.d.ts +1 -0
  44. package/lib/types/client/UpdateSettingsSection.d.ts +10 -0
  45. package/lib/types/client/UploadOverlay.d.ts +20 -0
  46. package/lib/types/client/UsageHeatmapSection.d.ts +4 -0
  47. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  48. package/lib/types/client/api.d.ts +345 -0
  49. package/lib/types/client/binary-download.d.ts +12 -0
  50. package/lib/types/client/breakpoints.d.ts +17 -0
  51. package/lib/types/client/browser.d.ts +60 -0
  52. package/lib/types/client/builtins/index.d.ts +18 -0
  53. package/lib/types/client/builtins/tabs.d.ts +11 -0
  54. package/lib/types/client/builtins/viewers.d.ts +3 -0
  55. package/lib/types/client/chat-presentation.d.ts +13 -0
  56. package/lib/types/client/chunk-loader.d.ts +124 -0
  57. package/lib/types/client/chunks/editor.d.ts +9 -0
  58. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  59. package/lib/types/client/chunks/terminal.d.ts +9 -0
  60. package/lib/types/client/cm-themes.d.ts +24 -0
  61. package/lib/types/client/conversation-draft.d.ts +14 -0
  62. package/lib/types/client/desktop-env.d.ts +35 -0
  63. package/lib/types/client/editor-load.d.ts +66 -0
  64. package/lib/types/client/frame-batcher.d.ts +24 -0
  65. package/lib/types/client/icons.d.ts +66 -0
  66. package/lib/types/client/image-types.d.ts +3 -0
  67. package/lib/types/client/ime-guard.d.ts +36 -0
  68. package/lib/types/client/index.d.ts +19 -0
  69. package/lib/types/client/intercept.d.ts +44 -0
  70. package/lib/types/client/lang.d.ts +14 -0
  71. package/lib/types/client/layout-push.d.ts +15 -0
  72. package/lib/types/client/lazy-chunk.d.ts +25 -0
  73. package/lib/types/client/link-intercept.d.ts +42 -0
  74. package/lib/types/client/locales-ar.d.ts +18 -0
  75. package/lib/types/client/locales-de.d.ts +4 -0
  76. package/lib/types/client/locales-fr.d.ts +10 -0
  77. package/lib/types/client/locales-hi.d.ts +17 -0
  78. package/lib/types/client/locales-id.d.ts +15 -0
  79. package/lib/types/client/locales-it.d.ts +8 -0
  80. package/lib/types/client/locales-ja.d.ts +17 -0
  81. package/lib/types/client/locales-ko.d.ts +9 -0
  82. package/lib/types/client/locales-nl.d.ts +16 -0
  83. package/lib/types/client/locales-pl.d.ts +19 -0
  84. package/lib/types/client/locales-pt.d.ts +1 -0
  85. package/lib/types/client/locales-ru.d.ts +13 -0
  86. package/lib/types/client/locales-sv.d.ts +1 -0
  87. package/lib/types/client/locales-th.d.ts +17 -0
  88. package/lib/types/client/locales-tr.d.ts +17 -0
  89. package/lib/types/client/locales-vi.d.ts +17 -0
  90. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  91. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  92. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  93. package/lib/types/client/locales.d.ts +571 -0
  94. package/lib/types/client/markdown-html.d.ts +96 -0
  95. package/lib/types/client/markdown-images.d.ts +45 -0
  96. package/lib/types/client/markdown-labels.d.ts +21 -0
  97. package/lib/types/client/md-toc.d.ts +4 -0
  98. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  99. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  100. package/lib/types/client/mermaid.d.ts +12 -0
  101. package/lib/types/client/open-when-sized.d.ts +20 -0
  102. package/lib/types/client/open-with-settings.d.ts +4 -0
  103. package/lib/types/client/open-with.d.ts +87 -0
  104. package/lib/types/client/openpath-intercept.d.ts +52 -0
  105. package/lib/types/client/paths.d.ts +43 -0
  106. package/lib/types/client/pdf-types.d.ts +2 -0
  107. package/lib/types/client/pinned.d.ts +86 -0
  108. package/lib/types/client/plugin-settings.d.ts +8 -0
  109. package/lib/types/client/plugins-shared.d.ts +27 -0
  110. package/lib/types/client/plugins-tabs.d.ts +3 -0
  111. package/lib/types/client/plugins-viewers.d.ts +3 -0
  112. package/lib/types/client/prefs.d.ts +41 -0
  113. package/lib/types/client/produced-files.d.ts +29 -0
  114. package/lib/types/client/selection-payload.d.ts +27 -0
  115. package/lib/types/client/service.d.ts +445 -0
  116. package/lib/types/client/session-pins.d.ts +14 -0
  117. package/lib/types/client/settings-nav-icon.d.ts +27 -0
  118. package/lib/types/client/shell-presets.d.ts +48 -0
  119. package/lib/types/client/split-pane.d.ts +38 -0
  120. package/lib/types/client/state.d.ts +480 -0
  121. package/lib/types/client/tab-content-memo.d.ts +40 -0
  122. package/lib/types/client/terminal-font.d.ts +78 -0
  123. package/lib/types/client/terminal-links.d.ts +119 -0
  124. package/lib/types/client/theme.d.ts +45 -0
  125. package/lib/types/client/titlebar-strip.d.ts +23 -0
  126. package/lib/types/client/ui-tweaks/appicons.d.ts +16 -0
  127. package/lib/types/client/ui-tweaks/archive.d.ts +42 -0
  128. package/lib/types/client/ui-tweaks/branding.d.ts +45 -0
  129. package/lib/types/client/ui-tweaks/cachehit.d.ts +41 -0
  130. package/lib/types/client/ui-tweaks/gitbar.d.ts +103 -0
  131. package/lib/types/client/ui-tweaks/index.d.ts +279 -0
  132. package/lib/types/client/ui-tweaks/mcp.d.ts +28 -0
  133. package/lib/types/client/ui-tweaks/notifier.d.ts +125 -0
  134. package/lib/types/client/ui-tweaks/whale.d.ts +55 -0
  135. package/lib/types/client/upload.d.ts +69 -0
  136. package/lib/types/client/useChatcodeAccount.d.ts +10 -0
  137. package/lib/types/client/wco.d.ts +47 -0
  138. package/lib/types/codingplan.d.ts +37 -0
  139. package/lib/types/config.d.ts +95 -0
  140. package/lib/types/context-types.d.ts +540 -0
  141. package/lib/types/fs-operations.d.ts +30 -0
  142. package/lib/types/fs-search.d.ts +24 -0
  143. package/lib/types/fs-tree.d.ts +51 -0
  144. package/lib/types/git.d.ts +124 -0
  145. package/lib/types/html-route.d.ts +59 -0
  146. package/lib/types/index.d.ts +47 -0
  147. package/lib/types/invariant.d.ts +15 -0
  148. package/lib/types/login-route.d.ts +2 -0
  149. package/lib/types/open-external.d.ts +24 -0
  150. package/lib/types/path-security.d.ts +26 -0
  151. package/lib/types/prefs-shared.d.ts +257 -0
  152. package/lib/types/pty-deps.d.ts +79 -0
  153. package/lib/types/pty-manager.d.ts +168 -0
  154. package/lib/types/tools.d.ts +34 -0
  155. package/lib/types/trust-fence.d.ts +24 -0
  156. package/lib/types/ui-tweaks/archive.d.ts +71 -0
  157. package/lib/types/ui-tweaks/config.d.ts +186 -0
  158. package/lib/types/ui-tweaks/git-web.d.ts +43 -0
  159. package/lib/types/ui-tweaks/git.d.ts +311 -0
  160. package/lib/types/ui-tweaks/index.d.ts +17 -0
  161. package/lib/types/ui-tweaks/mcp.d.ts +103 -0
  162. package/lib/types/ui-tweaks/turn-outcome.d.ts +57 -0
  163. package/lib/types/ui-tweaks/web.d.ts +55 -0
  164. package/lib/types/wire.d.ts +38 -0
  165. package/package.json +213 -0
  166. package/scripts/install.ps1 +310 -0
  167. package/scripts/install.sh +287 -0
  168. package/src/agent-opens.ts +265 -0
  169. package/src/agent-pty.ts +527 -0
  170. package/src/browser-probe.ts +26 -0
  171. package/src/bundle-route.ts +129 -0
  172. package/src/chatcode-update.ts +193 -0
  173. package/src/client/AccountMenu.tsx +177 -0
  174. package/src/client/AiRatioSection.module.css +329 -0
  175. package/src/client/AiRatioSection.tsx +292 -0
  176. package/src/client/BrowserView.tsx +289 -0
  177. package/src/client/ChatcodeGate.tsx +35 -0
  178. package/src/client/CodingPlanModelsSettings.module.css +118 -0
  179. package/src/client/CodingPlanModelsSettings.tsx +101 -0
  180. package/src/client/DiffTab.tsx +114 -0
  181. package/src/client/DiffView.tsx +305 -0
  182. package/src/client/EditorHost.tsx +563 -0
  183. package/src/client/FenceErrorNotice.tsx +40 -0
  184. package/src/client/FileTree.tsx +701 -0
  185. package/src/client/FreeWindow.tsx +337 -0
  186. package/src/client/GitView.tsx +722 -0
  187. package/src/client/MarkdownHtml.tsx +296 -0
  188. package/src/client/OrphanedTab.tsx +27 -0
  189. package/src/client/PdfView.tsx +110 -0
  190. package/src/client/ReleaseNotesDialog.tsx +91 -0
  191. package/src/client/RenderBoundary.tsx +49 -0
  192. package/src/client/SandboxStatusBar.tsx +60 -0
  193. package/src/client/SideCardSection.module.css +932 -0
  194. package/src/client/SideCardSection.tsx +1169 -0
  195. package/src/client/Sidebar.tsx +1857 -0
  196. package/src/client/TabBar.tsx +365 -0
  197. package/src/client/TerminalView.tsx +424 -0
  198. package/src/client/TextEditor.tsx +468 -0
  199. package/src/client/TreePanel.tsx +279 -0
  200. package/src/client/UpdateDialog.tsx +186 -0
  201. package/src/client/UpdateSettingsPanel.tsx +29 -0
  202. package/src/client/UpdateSettingsSection.tsx +22 -0
  203. package/src/client/UploadOverlay.tsx +62 -0
  204. package/src/client/UsageHeatmapSection.module.css +440 -0
  205. package/src/client/UsageHeatmapSection.tsx +301 -0
  206. package/src/client/add-plugin-modal.tsx +199 -0
  207. package/src/client/api.ts +433 -0
  208. package/src/client/binary-download.tsx +23 -0
  209. package/src/client/breakpoints.ts +61 -0
  210. package/src/client/browser.ts +167 -0
  211. package/src/client/builtins/index.ts +37 -0
  212. package/src/client/builtins/tabs.tsx +275 -0
  213. package/src/client/builtins/viewers.tsx +125 -0
  214. package/src/client/chat-presentation.ts +95 -0
  215. package/src/client/chunk-loader.ts +350 -0
  216. package/src/client/chunks/editor.tsx +9 -0
  217. package/src/client/chunks/mermaid.tsx +10 -0
  218. package/src/client/chunks/terminal.tsx +9 -0
  219. package/src/client/cm-themes.ts +129 -0
  220. package/src/client/conversation-draft.ts +29 -0
  221. package/src/client/css-modules.d.ts +5 -0
  222. package/src/client/desktop-env.ts +72 -0
  223. package/src/client/editor-load.ts +92 -0
  224. package/src/client/frame-batcher.ts +56 -0
  225. package/src/client/icons.tsx +170 -0
  226. package/src/client/image-types.ts +8 -0
  227. package/src/client/ime-guard.ts +47 -0
  228. package/src/client/index.tsx +501 -0
  229. package/src/client/intercept.tsx +172 -0
  230. package/src/client/lang.ts +225 -0
  231. package/src/client/layout-push.ts +37 -0
  232. package/src/client/layout.css +156 -0
  233. package/src/client/lazy-chunk.tsx +89 -0
  234. package/src/client/link-intercept.ts +73 -0
  235. package/src/client/locales-ar.ts +387 -0
  236. package/src/client/locales-de.ts +372 -0
  237. package/src/client/locales-fr.ts +379 -0
  238. package/src/client/locales-hi.ts +386 -0
  239. package/src/client/locales-id.ts +384 -0
  240. package/src/client/locales-it.ts +377 -0
  241. package/src/client/locales-ja.ts +486 -0
  242. package/src/client/locales-ko.ts +378 -0
  243. package/src/client/locales-nl.ts +384 -0
  244. package/src/client/locales-pl.ts +388 -0
  245. package/src/client/locales-pt.ts +369 -0
  246. package/src/client/locales-ru.ts +384 -0
  247. package/src/client/locales-sv.ts +369 -0
  248. package/src/client/locales-th.ts +386 -0
  249. package/src/client/locales-tr.ts +386 -0
  250. package/src/client/locales-vi.ts +386 -0
  251. package/src/client/locales-zh-HK.ts +401 -0
  252. package/src/client/locales-zh-MO.ts +401 -0
  253. package/src/client/locales-zh-TW.ts +401 -0
  254. package/src/client/locales.ts +1229 -0
  255. package/src/client/markdown-html.ts +331 -0
  256. package/src/client/markdown-images.ts +137 -0
  257. package/src/client/markdown-labels.tsx +31 -0
  258. package/src/client/md-toc.tsx +127 -0
  259. package/src/client/mermaid-blocks.ts +110 -0
  260. package/src/client/mermaid-sanitize.ts +93 -0
  261. package/src/client/mermaid.tsx +406 -0
  262. package/src/client/open-when-sized.ts +43 -0
  263. package/src/client/open-with-settings.tsx +135 -0
  264. package/src/client/open-with.ts +221 -0
  265. package/src/client/openpath-intercept.ts +79 -0
  266. package/src/client/paths.ts +63 -0
  267. package/src/client/pdf-types.ts +4 -0
  268. package/src/client/pinned.ts +184 -0
  269. package/src/client/plugin-settings.ts +43 -0
  270. package/src/client/plugins-shared.ts +29 -0
  271. package/src/client/plugins-tabs.ts +153 -0
  272. package/src/client/plugins-viewers.ts +44 -0
  273. package/src/client/prefs.ts +216 -0
  274. package/src/client/produced-files.ts +120 -0
  275. package/src/client/selection-payload.ts +86 -0
  276. package/src/client/service.ts +873 -0
  277. package/src/client/session-pins.tsx +334 -0
  278. package/src/client/settings-nav-icon.ts +55 -0
  279. package/src/client/shell-presets.ts +83 -0
  280. package/src/client/sidebar.module.css +3607 -0
  281. package/src/client/split-pane.tsx +333 -0
  282. package/src/client/state.ts +1646 -0
  283. package/src/client/tab-content-memo.ts +55 -0
  284. package/src/client/terminal-font.ts +224 -0
  285. package/src/client/terminal-links.ts +194 -0
  286. package/src/client/theme.ts +110 -0
  287. package/src/client/titlebar-strip.ts +37 -0
  288. package/src/client/ui-tweaks/appicons.ts +24 -0
  289. package/src/client/ui-tweaks/archive.tsx +281 -0
  290. package/src/client/ui-tweaks/branding.tsx +232 -0
  291. package/src/client/ui-tweaks/cachehit.tsx +122 -0
  292. package/src/client/ui-tweaks/gitbar.tsx +2559 -0
  293. package/src/client/ui-tweaks/index.tsx +1696 -0
  294. package/src/client/ui-tweaks/mcp.tsx +582 -0
  295. package/src/client/ui-tweaks/notifier.ts +514 -0
  296. package/src/client/ui-tweaks/whale.tsx +171 -0
  297. package/src/client/upload.ts +187 -0
  298. package/src/client/useChatcodeAccount.ts +103 -0
  299. package/src/client/wco.ts +120 -0
  300. package/src/codingplan.ts +146 -0
  301. package/src/config.ts +207 -0
  302. package/src/context-types.ts +559 -0
  303. package/src/fs-operations.ts +97 -0
  304. package/src/fs-search.ts +113 -0
  305. package/src/fs-tree.ts +158 -0
  306. package/src/git.ts +446 -0
  307. package/src/html-route.ts +106 -0
  308. package/src/index.ts +1524 -0
  309. package/src/invariant.ts +32 -0
  310. package/src/login-route.ts +26 -0
  311. package/src/open-external.ts +90 -0
  312. package/src/path-security.ts +81 -0
  313. package/src/prefs-shared.ts +303 -0
  314. package/src/pty-deps.ts +241 -0
  315. package/src/pty-manager.ts +376 -0
  316. package/src/tools.ts +482 -0
  317. package/src/trust-fence.ts +84 -0
  318. package/src/ui-tweaks/archive.ts +293 -0
  319. package/src/ui-tweaks/config.ts +257 -0
  320. package/src/ui-tweaks/git-web.ts +421 -0
  321. package/src/ui-tweaks/git.ts +1524 -0
  322. package/src/ui-tweaks/index.ts +61 -0
  323. package/src/ui-tweaks/mcp.ts +535 -0
  324. package/src/ui-tweaks/turn-outcome.ts +105 -0
  325. package/src/ui-tweaks/web.ts +225 -0
  326. package/src/wire.ts +110 -0
  327. package/vendor/addon-fit.js +2 -0
  328. package/vendor/xterm.css +218 -0
  329. package/vendor/xterm.js +5 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * A non-invasive extension of DSH's native Workspace browser.
3
+ *
4
+ * `sidebar.workspaces` is a single-owner host slot, so replacing its occupant
5
+ * would also mean reimplementing search, workspace creation, ordering and all
6
+ * of the native dialogs. Instead this module mounts one small React island as
7
+ * a PEER of that slot and augments the existing row menu. The native browser
8
+ * therefore continues to own every pre-existing action.
9
+ */
10
+ import { createRoot, type Root } from 'react-dom/client'
11
+ import { useEffect, useState, useSyncExternalStore } from 'react'
12
+ import type { Context, SidebarSessionSummary } from '../context-types.ts'
13
+ import type { SidebarStore } from './state.ts'
14
+ import { api } from './api.ts'
15
+ import { parsePrefs } from './prefs.ts'
16
+ import { t } from './locales.ts'
17
+ import { IconPinOutline16 } from './icons.tsx'
18
+ import css from './sidebar.module.css'
19
+
20
+ const PIN_SETTINGS_KEY = 'session-pins'
21
+
22
+ /** Read and sanitize the plugin-owned durable pin order. */
23
+ export function pinnedSessionIdsOf(value: { pluginSettings: Record<string, Record<string, unknown>> }): string[] {
24
+ const raw = value.pluginSettings[PIN_SETTINGS_KEY]?.ids
25
+ if (!Array.isArray(raw)) return []
26
+ const seen = new Set<string>()
27
+ return raw.filter((id): id is string => {
28
+ if (typeof id !== 'string' || id === '' || seen.has(id)) return false
29
+ seen.add(id)
30
+ return true
31
+ })
32
+ }
33
+
34
+ function pinSettings(ids: readonly string[]): Record<string, unknown> {
35
+ return { ids: [...ids] }
36
+ }
37
+
38
+ /** A small current-color pin icon for the menu row injected into DSH's menu. */
39
+ function pinIconMarkup(): string {
40
+ return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M9.5 1.5 14.5 6.5 12.5 8.5 10 6 5.5 10.5 6 12 4.5 13.5 2.5 11.5 4 10 5.5 10.5 10 6 7.5 8.5 6.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>'
41
+ }
42
+
43
+ function isSessionRow(element: Element | null): element is HTMLElement {
44
+ return element instanceof HTMLElement && element.matches('[class*="_sessionRow"]')
45
+ }
46
+
47
+ /**
48
+ * The native row deliberately has no session-id DOM attribute. React retains
49
+ * the row component's `node` prop on its fiber, however; read that identity
50
+ * rather than guessing from the title (automatic titles can be duplicated).
51
+ */
52
+ function sessionIdFromReactRow(row: HTMLElement): string | undefined {
53
+ const record = row as unknown as Record<string, unknown>
54
+ const fiberKey = Object.keys(record).find(key => key.startsWith('__reactFiber$'))
55
+ let fiber = fiberKey === undefined ? undefined : record[fiberKey] as Record<string, unknown> | undefined
56
+ const seen = new Set<object>()
57
+ while (fiber !== undefined && !seen.has(fiber)) {
58
+ seen.add(fiber)
59
+ const props = fiber.memoizedProps as Record<string, unknown> | undefined
60
+ const node = props?.node as Record<string, unknown> | undefined
61
+ if (typeof node?.id === 'string') return node.id
62
+ fiber = fiber.return as Record<string, unknown> | undefined
63
+ }
64
+ return undefined
65
+ }
66
+
67
+ /** Keep native Workspace rows hidden only while their session is pinned. */
68
+ function syncNativePinnedRows(ids: readonly string[]): void {
69
+ const pinned = new Set(ids)
70
+ for (const row of document.querySelectorAll<HTMLElement>('[data-slot="sidebar.workspaces"] [class*="_sessionRow"]')) {
71
+ const sessionId = sessionIdFromReactRow(row)
72
+ if (sessionId !== undefined && pinned.has(sessionId)) row.dataset.dshSessionPinned = ''
73
+ else delete row.dataset.dshSessionPinned
74
+ }
75
+ }
76
+
77
+ function menuNear(anchor: HTMLElement): HTMLElement | undefined {
78
+ const rect = anchor.getBoundingClientRect()
79
+ return [...document.querySelectorAll<HTMLElement>('[role="menu"]')].find((menu) => {
80
+ if (menu.dataset.dshSessionPinMenu === 'true') return false
81
+ const items = menu.querySelectorAll(':scope button[role="menuitem"]')
82
+ if (items.length !== 3 || menu.getClientRects().length === 0) return false
83
+ const menuRect = menu.getBoundingClientRect()
84
+ return Math.abs(menuRect.top - rect.top) < 260 && Math.abs(menuRect.left - rect.right) < 360
85
+ })
86
+ }
87
+
88
+ /**
89
+ * Mount the separate pinned group and append a fourth item to DSH's native
90
+ * three-item row menu. The group is inserted beside (not inside) the host
91
+ * Workspace slot, so it is a top-level left-sidebar section. Both DOM seams
92
+ * are explicitly scoped to the current alpha host's stable slot and row class
93
+ * suffixes; failure is fail-open and leaves the native Workspace UI untouched.
94
+ */
95
+ export function installSessionPins(ctx: Context, store: SidebarStore): () => void {
96
+ let root: Root | undefined
97
+ let mount: HTMLDivElement | undefined
98
+ let observer: MutationObserver | undefined
99
+ let write = Promise.resolve()
100
+ let rowSyncFrame: number | undefined
101
+
102
+ const scheduleNativeRowSync = (): void => {
103
+ if (rowSyncFrame !== undefined) return
104
+ rowSyncFrame = requestAnimationFrame(() => {
105
+ rowSyncFrame = undefined
106
+ syncNativePinnedRows(pinnedSessionIdsOf(store.getPrefs()))
107
+ })
108
+ }
109
+
110
+ const updatePins = (nextIds: readonly string[]): void => {
111
+ const before = store.getPrefs()
112
+ const next = {
113
+ ...before,
114
+ pluginSettings: {
115
+ ...before.pluginSettings,
116
+ [PIN_SETTINGS_KEY]: pinSettings(nextIds),
117
+ },
118
+ }
119
+ store.setPrefs(next)
120
+ write = write.catch(() => undefined).then(async () => {
121
+ const current = store.getPrefs()
122
+ const view = await api.settingsUpdate({
123
+ pluginSettings: {
124
+ ...current.pluginSettings,
125
+ [PIN_SETTINGS_KEY]: pinSettings(nextIds),
126
+ },
127
+ })
128
+ store.setPrefs(parsePrefs(view.value))
129
+ }).catch((error: unknown) => {
130
+ // Do not roll back a later toggle if this older write fails.
131
+ if (pinnedSessionIdsOf(store.getPrefs()).join('\0') === nextIds.join('\0')) store.setPrefs(before)
132
+ console.warn('[dsh-better-sidebar] persist session pins failed:', error)
133
+ })
134
+ }
135
+
136
+ const sessionForRow = (row: HTMLElement): SidebarSessionSummary | undefined => {
137
+ const id = sessionIdFromReactRow(row)
138
+ if (id === undefined) return undefined
139
+ const list = ctx.sessions.list.getSnapshot()
140
+ const session = list.byId[id]
141
+ return session?.origin === 'subagent' ? undefined : session
142
+ }
143
+
144
+ const decorateMenu = (anchor: HTMLElement, sessionId: string): void => {
145
+ const menu = menuNear(anchor)
146
+ if (menu === undefined) return
147
+ const template = menu.querySelector<HTMLButtonElement>(':scope button[role="menuitem"]')
148
+ if (template === null || template === undefined) return
149
+ menu.dataset.dshSessionPinMenu = 'true'
150
+ const item = template.cloneNode(true) as HTMLButtonElement
151
+ item.dataset.dshSessionPinAction = sessionId
152
+ const pinned = pinnedSessionIdsOf(store.getPrefs()).includes(sessionId)
153
+ const label = item.querySelector<HTMLElement>('span[class*="_itemLabel"]')
154
+ if (label !== null) label.textContent = pinned ? t('unpinSession') : t('pinSession')
155
+ else item.textContent = pinned ? t('unpinSession') : t('pinSession')
156
+ const icon = item.querySelector<HTMLElement>('span[class*="_itemIcon"]')
157
+ if (icon !== null) icon.innerHTML = pinIconMarkup()
158
+ item.addEventListener('click', (event) => {
159
+ event.preventDefault()
160
+ event.stopPropagation()
161
+ const ids = pinnedSessionIdsOf(store.getPrefs())
162
+ updatePins(ids.includes(sessionId) ? ids.filter(id => id !== sessionId) : [...ids, sessionId])
163
+ // The portal is owned by DSH React. Never remove it ourselves: doing
164
+ // so leaves React attempting to unmount an already-detached node and
165
+ // can take the whole Workspace browser down on its next render. Its
166
+ // standard outside-pointer listener closes the menu safely.
167
+ document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }))
168
+ })
169
+ menu.appendChild(item)
170
+ }
171
+
172
+ const onDocumentClick = (event: MouseEvent): void => {
173
+ const button = (event.target as Element | null)?.closest('button')
174
+ const row = button?.closest('[class*="_sessionRow"]')
175
+ if (row === null || row === undefined || !isSessionRow(row) || button === null || button === undefined) return
176
+ const session = sessionForRow(row)
177
+ if (session === undefined) return
178
+ // DSH creates its portalled menu during this click's React update.
179
+ requestAnimationFrame(() => { decorateMenu(button, session.id) })
180
+ }
181
+
182
+ const attach = (): void => {
183
+ const slot = document.querySelector<HTMLElement>('[data-slot="sidebar.workspaces"]')
184
+ const region = slot?.parentElement
185
+ if (slot === null || slot === undefined || !(region instanceof HTMLElement) || mount?.parentElement === region) return
186
+ root?.unmount()
187
+ mount?.remove()
188
+ mount = document.createElement('div')
189
+ mount.dataset.dshPinnedSessions = ''
190
+ // The slot wrapper is `display: contents`; placing this mount immediately
191
+ // before it makes the pin group a peer of the Workspace browser in the
192
+ // sidebar's flex column, not a child of the browser's session list.
193
+ region.insertBefore(mount, slot)
194
+ root = createRoot(mount)
195
+ root.render(<PinnedSessions ctx={ctx} store={store} onChange={updatePins} />)
196
+ }
197
+
198
+ observer = new MutationObserver(() => {
199
+ // Conversation streaming mutates the page continuously. Once our island
200
+ // is attached there is nothing to scan; only a host re-render that removes
201
+ // it needs a fresh lookup.
202
+ if (!mount?.isConnected) attach()
203
+ if (pinnedSessionIdsOf(store.getPrefs()).length > 0) scheduleNativeRowSync()
204
+ })
205
+ observer.observe(document.body, { childList: true, subtree: true })
206
+ attach()
207
+ document.addEventListener('click', onDocumentClick, true)
208
+ return () => {
209
+ document.removeEventListener('click', onDocumentClick, true)
210
+ observer?.disconnect()
211
+ if (rowSyncFrame !== undefined) cancelAnimationFrame(rowSyncFrame)
212
+ syncNativePinnedRows([])
213
+ root?.unmount()
214
+ mount?.remove()
215
+ }
216
+ }
217
+
218
+ function PinnedSessions({ ctx, store, onChange }: {
219
+ ctx: Context
220
+ store: SidebarStore
221
+ onChange: (ids: readonly string[]) => void
222
+ }) {
223
+ const list = useSyncExternalStore(ctx.sessions.list.subscribe, ctx.sessions.list.getSnapshot, ctx.sessions.list.getSnapshot)
224
+ const prefs = useSyncExternalStore(store.subscribe.bind(store), store.getSnapshot.bind(store), store.getSnapshot.bind(store)).prefs
225
+ const ids = pinnedSessionIdsOf(prefs)
226
+ const pinKey = ids.join('\0')
227
+ const sessions = ids.flatMap((id) => {
228
+ const session = list.byId[id]
229
+ return session === undefined || session.origin === 'subagent' ? [] : [session]
230
+ })
231
+ const [menuFor, setMenuFor] = useState<string | undefined>()
232
+
233
+ // Purge only definitively vanished rows; a temporarily warming list must
234
+ // never erase a durable pin just after startup.
235
+ useEffect(() => {
236
+ if (sessions.length === ids.length || Object.keys(list.byId).length === 0) return
237
+ onChange(sessions.map(session => session.id))
238
+ }, [ids.length, list.byId, onChange, sessions])
239
+
240
+ useEffect(() => {
241
+ syncNativePinnedRows(ids)
242
+ return () => { syncNativePinnedRows([]) }
243
+ }, [pinKey])
244
+
245
+ const closeMenu = (): void => { setMenuFor(undefined) }
246
+
247
+ const renameSession = (session: SidebarSessionSummary): void => {
248
+ const title = window.prompt(t('pinnedSessionRenamePrompt'), session.displayTitle)?.trim()
249
+ if (title === undefined || title === '') return
250
+ closeMenu()
251
+ void ctx.sessions.binding?.(session.id)?.session.rename(title).catch((error: unknown) => {
252
+ console.warn('[dsh-better-sidebar] rename pinned session failed:', error)
253
+ })
254
+ }
255
+
256
+ const forkSession = (sessionId: string): void => {
257
+ closeMenu()
258
+ void ctx.sessions.fork?.({ sessionId, increaseTitle: true }).then((childId) => {
259
+ ctx.sessions.open?.(childId)
260
+ }).catch((error: unknown) => {
261
+ console.warn('[dsh-better-sidebar] fork pinned session failed:', error)
262
+ })
263
+ }
264
+
265
+ const archiveSession = (sessionId: string): void => {
266
+ closeMenu()
267
+ // `archiveSession` is a native Workspace browser verb. It is intentionally
268
+ // structural here: this island reuses the host action rather than owning
269
+ // a parallel archive implementation.
270
+ const archive = (ctx.workspaces as typeof ctx.workspaces & {
271
+ archiveSession?: (id: string) => Promise<void>
272
+ }).archiveSession
273
+ if (archive === undefined) return
274
+ void archive(sessionId).then(() => {
275
+ onChange(ids.filter(id => id !== sessionId))
276
+ }).catch((error: unknown) => {
277
+ console.warn('[dsh-better-sidebar] archive pinned session failed:', error)
278
+ })
279
+ }
280
+
281
+ if (sessions.length === 0) return null
282
+ return (
283
+ <section className={css.pinnedSessions} aria-label={t('pinnedSessions')}>
284
+ <div className={css.pinnedSessionsHeading}>
285
+ <span>{t('pinnedSessions')}</span>
286
+ </div>
287
+ <div className={css.pinnedSessionsList} role="list">
288
+ {sessions.map(session => (
289
+ <div className={css.pinnedSessionRow} key={session.id} role="listitem">
290
+ <button
291
+ type="button"
292
+ className={css.pinnedSessionOpen}
293
+ aria-current={session.id === list.current ? 'page' : undefined}
294
+ onClick={() => { ctx.sessions.open?.(session.id) }}
295
+ >
296
+ <span className={css.pinnedSessionDot} data-running={session.running || undefined} aria-hidden="true" />
297
+ <span className={css.pinnedSessionTitle}>{session.displayTitle}</span>
298
+ </button>
299
+ <button
300
+ type="button"
301
+ className={css.pinnedSessionMore}
302
+ aria-label={t('unpinSession')}
303
+ aria-expanded={menuFor === session.id}
304
+ onClick={() => { setMenuFor(menuFor === session.id ? undefined : session.id) }}
305
+ >
306
+ <span aria-hidden="true">•••</span>
307
+ </button>
308
+ {menuFor === session.id && (
309
+ <div className={css.pinnedSessionMenu} role="menu">
310
+ <button type="button" role="menuitem" onClick={() => { renameSession(session) }}>
311
+ {t('pinnedSessionRename')}
312
+ </button>
313
+ <button type="button" role="menuitem" onClick={() => { forkSession(session.id) }}>
314
+ {t('pinnedSessionFork')}
315
+ </button>
316
+ <button type="button" role="menuitem" onClick={() => { archiveSession(session.id) }}>
317
+ {t('pinnedSessionArchive')}
318
+ </button>
319
+ <div className={css.pinnedSessionMenuDivider} role="separator" />
320
+ <button type="button" role="menuitem" onClick={() => {
321
+ onChange(ids.filter(id => id !== session.id))
322
+ closeMenu()
323
+ }}>
324
+ <IconPinOutline16 size={16} />
325
+ {t('unpinSession')}
326
+ </button>
327
+ </div>
328
+ )}
329
+ </div>
330
+ ))}
331
+ </div>
332
+ </section>
333
+ )
334
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Mark this plugin's row in the DSH settings navigation so its bundled CSS
3
+ * can replace the shell's fallback gear with the Side card glyph.
4
+ *
5
+ * DSH 0.1.x projects only `id`, `order`, and `label` from a
6
+ * `settings.section` registration, then chooses icons inside the settings
7
+ * shell from a closed list of built-in ids. Until that public contract grows
8
+ * an icon field, the plugin identifies only its own localized row after the
9
+ * dialog mounts. The marker owns no shell structure and is removed on fiber
10
+ * disposal, so the adaptation remains HMR-safe.
11
+ */
12
+
13
+ export const SETTINGS_NAV_MARKER = 'data-dsh-better-sidebar-settings-nav'
14
+
15
+ /** Marker of the "Token 用量" (usage heatmap) settings nav row. */
16
+ export const USAGE_NAV_MARKER = 'data-dsh-better-sidebar-usage-nav'
17
+ export const AI_RATIO_NAV_MARKER = 'data-dsh-better-sidebar-ai-ratio-nav'
18
+
19
+ /** Marker of the "更新" (update) settings nav row. */
20
+ export const UPDATE_NAV_MARKER = 'data-dsh-better-sidebar-update-nav'
21
+
22
+ /**
23
+ * Keep the marker on the settings-nav button whose visible text is this
24
+ * plugin's current localized section label.
25
+ * @param label - locale-aware label resolver used by the section registration.
26
+ * @param marker - the marker attribute this registration owns (each icon gets
27
+ * its own so several sections can mark their rows without stripping each
28
+ * other's markers).
29
+ * @returns disposer that disconnects observation and removes owned markers.
30
+ */
31
+ export function registerSettingsNavIcon(label: () => string, marker: string = SETTINGS_NAV_MARKER): () => void {
32
+ let disposed = false
33
+
34
+ const sync = (): void => {
35
+ if (disposed) return
36
+ const currentLabel = label().trim()
37
+ const buttons = document.querySelectorAll<HTMLButtonElement>('[role="dialog"] nav button')
38
+ for (const button of buttons) {
39
+ const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel
40
+ if (matches) button.setAttribute(marker, '')
41
+ else button.removeAttribute(marker)
42
+ }
43
+ }
44
+
45
+ sync()
46
+ const observer = new MutationObserver(sync)
47
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true })
48
+
49
+ return () => {
50
+ disposed = true
51
+ observer.disconnect()
52
+ document.querySelectorAll(`[${marker}]`)
53
+ .forEach(element => { element.removeAttribute(marker) })
54
+ }
55
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Built-in desktop-shell presets for the "位置兼容模式" secondary setting
3
+ * (scheme `preset`). DATA-DRIVEN by design: each shell's adaptation lives
4
+ * in one entry (strip values / optional CSS), the core applies it through
5
+ * the SAME generic strip variable and CSS-injection mechanism — adding a
6
+ * shell is adding data, never a code path.
7
+ *
8
+ * Inclusion rule (maintained in docs/external-plugin-guide.md §12): only shells that (a) appear
9
+ * in this repo's issues/PRs (a user actually hit a problem) and (b) have
10
+ * 100+ GitHub stars (a real user base). The mechanism is opt-in: auto
11
+ * detection never applies a preset — the settings badge only SUGGESTS it.
12
+ */
13
+ import type { DesktopEnv } from './desktop-env.ts'
14
+
15
+ export interface ShellPreset {
16
+ /** Stable preset id (persisted in `titleBarPresetId`). */
17
+ readonly id: string
18
+ /** User-facing name of the shell. */
19
+ readonly title: string
20
+ /** One-line description shown in the settings popup. */
21
+ readonly desc: string
22
+ /**
23
+ * The top strip (px) this shell reserves over web content, per
24
+ * environment — the fallback used when neither the standard WCO API nor
25
+ * the `dsh-desktop-titlebar-inset` contract parameter is available.
26
+ * Return undefined when the shell needs no strip in that environment.
27
+ * MUST be pure (called during render).
28
+ */
29
+ readonly stripFor?: (env: DesktopEnv) => number | undefined
30
+ /**
31
+ * Extra CSS applied while the preset is enabled (injected last, after the
32
+ * plugin's own styles). Targets the plugin's stable data attributes and
33
+ * shell-declared body/URL markers only — never other shells' class names.
34
+ * Empty for presets fully covered by the strip variable.
35
+ */
36
+ readonly css?: string
37
+ /**
38
+ * Whether this shell's marker is currently visible (URL stamps / preload
39
+ * markers — see desktop-env.ts). Used ONLY for the settings "已检测"
40
+ * badge; never auto-applies anything. MUST be pure.
41
+ */
42
+ readonly detect?: (env: DesktopEnv) => boolean
43
+ }
44
+
45
+ /**
46
+ * DeepSeek Harness Desktop (anywhere-labs, Electron, 16k+ stars — the most
47
+ * reported shell in this repo's issues/PRs). Advanced mode: macOS reserves
48
+ * a 20px caption row (traffic lights top-left), win32 draws the native
49
+ * window controls in a ~32px overlay (WCO reports the real height when
50
+ * available, which the auto scheme already consumes; the 32 is the
51
+ * no-WCO fallback). Compatibility mode keeps the native frame — nothing.
52
+ */
53
+ const DSH_DESKTOP: ShellPreset = {
54
+ id: 'dsh-desktop',
55
+ title: 'DeepSeek Harness Desktop',
56
+ desc: 'Electron 高级模式(无边框):macOS 顶栏 20px、Windows 无 WCO 时 32px 标题栏让位',
57
+ stripFor: (env) => {
58
+ if (env.mode !== 'advanced') return undefined
59
+ if (env.platform === 'darwin') return 20
60
+ if (env.platform === 'win32') return 32
61
+ return undefined
62
+ },
63
+ detect: (env) => env.mode === 'advanced',
64
+ }
65
+
66
+ const PRESETS: readonly ShellPreset[] = [
67
+ DSH_DESKTOP,
68
+ ]
69
+
70
+ /** All built-in shell presets (registration order = settings list order). */
71
+ export function getShellPresets(): readonly ShellPreset[] {
72
+ return PRESETS
73
+ }
74
+
75
+ /** One preset by id, or undefined for an unknown/empty id. */
76
+ export function getShellPreset(id: string): ShellPreset | undefined {
77
+ return PRESETS.find(preset => preset.id === id)
78
+ }
79
+
80
+ /** The strip the active preset contributes for the given environment. */
81
+ export function presetStripFor(preset: ShellPreset | undefined, env: DesktopEnv): number | undefined {
82
+ return preset?.stripFor?.(env)
83
+ }