@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,281 @@
1
+ /**
2
+ * dsh-ui-tweaks — Archive manager (browser half).
3
+ *
4
+ * Renders the "归档" settings section (shown in the Settings dialog; usable
5
+ * when the `archiveManagerEnabled` toggle in 界面调整 is on — otherwise it
6
+ * shows an invite card with a one-click enable). The section lists every
7
+ * archived session — enriched from the framework `useSessions` /
8
+ * `useWorkspaces` feeds — with per-row actions and batch actions that go
9
+ * through the same-origin archive route (`/_dsh/ui-tweaks/archive`):
10
+ *
11
+ * - **恢复** (Restore): remove the session from the archive set — its log and
12
+ * workspace slot are kept, so the conversation reappears in the normal
13
+ * sidebar list.
14
+ * - **删除** (Delete): PERMANENTLY delete the session — the server removes its
15
+ * durable log, workspace accounting, archive-set entry and caches; the list
16
+ * then refreshes so the row (and the session itself) disappear.
17
+ * - **全部恢复 / 全部删除**: the same actions for every archived session.
18
+ *
19
+ * Live (open/running) sessions are refused by the server; the client maps the
20
+ * `session-live` error code to localized copy.
21
+ * @module dsh-ui-tweaks/client/archive
22
+ */
23
+
24
+ import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
25
+ import type { ISessions, SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
26
+ import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
27
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
28
+ import type { SettingsClient } from './index.tsx'
29
+
30
+ /** Route matching the host half (src/archive.ts). */
31
+ const ARCHIVE_ROUTE = '/_dsh/ui-tweaks/archive'
32
+
33
+ /** Locale keys the Archive manager reads off the `ui-tweaks` dictionary. */
34
+ type ArchiveLabelKey =
35
+ | 'archiveNav' | 'archiveTitle' | 'archiveEmpty' | 'archiveRestore' | 'archiveRestoring'
36
+ | 'archiveDelete' | 'archiveDeleteAll' | 'archiveRestoreAll'
37
+ | 'archiveCount' | 'archiveUnavailable' | 'archiveLiveError' | 'archiveDisabledHint'
38
+ | 'archiveEnable' | 'archiveRestored' | 'unavailable'
39
+
40
+ type Translate = (key: ArchiveLabelKey) => string
41
+
42
+ interface ApiSuccess<T> { ok: true; value: T }
43
+ interface ApiFailure { ok: false; error: { code: string; message: string } }
44
+
45
+ interface ArchiveSnapshot {
46
+ archivedSessionIds: string[]
47
+ }
48
+
49
+ /** Fetch one archive route call, attaching the server error code when present. */
50
+ async function archiveRequest<T>(init?: RequestInit): Promise<T> {
51
+ const response = await fetch(ARCHIVE_ROUTE, { credentials: 'same-origin', ...init })
52
+ let body: ApiSuccess<T> | ApiFailure | undefined
53
+ try {
54
+ body = await response.json() as ApiSuccess<T> | ApiFailure
55
+ } catch {
56
+ // Non-JSON response (e.g. the route is not mounted yet) — fall through to
57
+ // a clean error instead of a JSON-parse exception.
58
+ body = undefined
59
+ }
60
+ if (!response.ok || body === undefined || !body.ok) {
61
+ const failure = body as ApiFailure | undefined
62
+ const error = new Error(failure?.error?.message ?? `UI Tweaks archive request failed with HTTP ${response.status}`) as Error & { code: string | undefined }
63
+ error.code = failure?.error?.code
64
+ throw error
65
+ }
66
+ return body.value
67
+ }
68
+
69
+ /** Map a thrown archive error to user-facing copy. */
70
+ function errorMessage(error: unknown, t: Translate): string {
71
+ const code = (error as { code?: string } | null)?.code
72
+ if (code === 'session-live') return t('archiveLiveError')
73
+ return error instanceof Error ? error.message : String(error)
74
+ }
75
+
76
+ /** Re-pull the host session list after a permanent delete (the outward
77
+ * ISessions face omits refresh; the concrete runtime provides it). */
78
+ function refreshSessions(sessionsService: ISessions): void {
79
+ void (sessionsService as unknown as { refresh(): Promise<void> }).refresh()
80
+ }
81
+
82
+ /** Workspace display title: the path's last non-empty segment. */
83
+ function basenameOf(path: string | undefined): string {
84
+ if (path === undefined || path === '') return ''
85
+ const cleaned = path.replace(/[\\/]+$/u, '')
86
+ const segments = cleaned.split(/[\\/]/u)
87
+ return segments.at(-1) ?? ''
88
+ }
89
+
90
+ /** Locale-aware relative time for a past timestamp, e.g. "3小时前". */
91
+ function relativeTime(ms: number, now: number, formatter: Intl.RelativeTimeFormat): string {
92
+ const diffSeconds = Math.round((ms - now) / 1000)
93
+ const abs = Math.abs(diffSeconds)
94
+ if (abs < 60) return formatter.format(diffSeconds, 'second')
95
+ if (abs < 3600) return formatter.format(Math.round(diffSeconds / 60), 'minute')
96
+ if (abs < 86400) return formatter.format(Math.round(diffSeconds / 3600), 'hour')
97
+ if (abs < 604800) return formatter.format(Math.round(diffSeconds / 86400), 'day')
98
+ return formatter.format(Math.round(diffSeconds / 604800), 'week')
99
+ }
100
+
101
+ export const ARCHIVE_CSS = `
102
+ .dut-arc{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
103
+ .dut-arc-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
104
+ .dut-arc-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
105
+ .dut-arc-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
106
+ .dut-arc-spacer{flex:1}
107
+ .dut-arc-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
108
+ .dut-arc-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
109
+ .dut-arc-btn:disabled{opacity:.5;cursor:default}
110
+ .dut-arc-btn.dut-arc-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
111
+ .dut-arc-btn.dut-arc-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
112
+ .dut-arc-body{display:grid;gap:2px;margin-top:4px}
113
+ .dut-arc-row{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:12px}
114
+ .dut-arc-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
115
+ .dut-arc-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;background:transparent;border:none;padding:0;cursor:default;color:var(--dsw-alias-label-primary);font:inherit}
116
+ .dut-arc-title{font-size:13.5px;font-weight:600;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
117
+ .dut-arc-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
118
+ .dut-arc-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
119
+ .dut-arc-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
120
+ .dut-arc-off{padding:20px 16px;display:grid;gap:12px}
121
+ .dut-arc-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
122
+ `
123
+
124
+ /** Install the Archive stylesheet once (idempotent); returns the disposer. */
125
+ export function installArchiveStyles(): () => void {
126
+ const id = 'dsh-ui-tweaks-archive'
127
+ const existing = document.querySelector(`style[data-plugin-css="${id}"]`)
128
+ if (existing !== null) return () => {}
129
+ const style = document.createElement('style')
130
+ style.dataset.plugin = 'dsh-ui-tweaks'
131
+ style.dataset.pluginCss = id
132
+ style.textContent = ARCHIVE_CSS
133
+ document.head.appendChild(style)
134
+ return () => { style.remove() }
135
+ }
136
+
137
+ /** One archived session row: the id plus the enriched session summary, when listed. */
138
+ interface ArchiveRow {
139
+ id: string
140
+ summary: SessionSummary | undefined
141
+ }
142
+
143
+ export interface ArchiveSectionProps {
144
+ controller: SettingsClient
145
+ t: Translate
146
+ sessionsService: ISessions
147
+ /** Framework standard feeds: session list + workspace archive set. */
148
+ useSessions: SnapshotSelectorHook<SessionListState>
149
+ useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>
150
+ }
151
+
152
+ export function ArchiveSection({ controller, t, sessionsService, useSessions, useWorkspaces }: ArchiveSectionProps) {
153
+ const settingsState = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot)
154
+ const enabled = settingsState.value?.archiveManagerEnabled === true
155
+ const sessions = useSessions(state => state)
156
+ const workspaces = useWorkspaces(state => state)
157
+ const [confirmId, setConfirmId] = useState<string | null>(null)
158
+ const [confirmAll, setConfirmAll] = useState(false)
159
+ const [busy, setBusy] = useState<string | null>(null)
160
+ const [error, setError] = useState<string | null>(null)
161
+ const timeFormatter = useMemo(() => new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' }), [])
162
+ const now = Date.now()
163
+
164
+ useEffect(() => {
165
+ if (confirmId === null) return
166
+ const timer = window.setTimeout(() => { setConfirmId(current => (current === confirmId ? null : current)) }, 3000)
167
+ return () => { window.clearTimeout(timer) }
168
+ }, [confirmId])
169
+ useEffect(() => {
170
+ if (!confirmAll) return
171
+ const timer = window.setTimeout(() => { setConfirmAll(false) }, 3000)
172
+ return () => { window.clearTimeout(timer) }
173
+ }, [confirmAll])
174
+
175
+ const rows: ArchiveRow[] = useMemo(() => {
176
+ const byId = sessions.byId
177
+ return workspaces.archivedSessionIds.map(id => ({ id, summary: byId[id] })).sort((a, b) => {
178
+ const ta = a.summary?.updatedAt ?? 0
179
+ const tb = b.summary?.updatedAt ?? 0
180
+ return tb - ta
181
+ })
182
+ }, [workspaces.archivedSessionIds, sessions.byId])
183
+
184
+ const run = (key: string, body: unknown, after?: () => void): void => {
185
+ setBusy(key)
186
+ setError(null)
187
+ void archiveRequest<ArchiveSnapshot>({
188
+ method: 'POST',
189
+ headers: { 'Content-Type': 'application/json' },
190
+ body: JSON.stringify(body),
191
+ }).then(() => { after?.() }).catch((reason: unknown) => {
192
+ setError(errorMessage(reason, t))
193
+ }).finally(() => { setBusy(current => (current === key ? null : current)) })
194
+ }
195
+
196
+ const restore = (id: string): void => {
197
+ run(`restore:${id}`, { action: 'restore', sessionId: id })
198
+ }
199
+
200
+ const remove = (id: string): void => {
201
+ if (confirmId !== id) {
202
+ setConfirmId(id)
203
+ return
204
+ }
205
+ setConfirmId(null)
206
+ run(`delete:${id}`, { action: 'delete', sessionId: id }, () => { refreshSessions(sessionsService) })
207
+ }
208
+
209
+ const restoreAll = (): void => {
210
+ run('restore-all', { action: 'restore-all' })
211
+ }
212
+
213
+ const removeAll = (): void => {
214
+ if (!confirmAll) {
215
+ setConfirmAll(true)
216
+ return
217
+ }
218
+ setConfirmAll(false)
219
+ run('delete-all', { action: 'delete-all' }, () => { refreshSessions(sessionsService) })
220
+ }
221
+
222
+ if (!enabled) {
223
+ return (
224
+ <div className="dut-arc">
225
+ <div className="dut-panel dut-arc-off">
226
+ <p>{t('archiveDisabledHint')}</p>
227
+ <div>
228
+ <button type="button" className="dut-btn dut-btn-active" onClick={() => {
229
+ void controller.set('archiveManagerEnabled', true).catch(() => { setError(t('unavailable')) })
230
+ }}>{t('archiveEnable')}</button>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ )
235
+ }
236
+
237
+ return (
238
+ <div className="dut-arc">
239
+ <div className="dut-panel">
240
+ <div className="dut-arc-head">
241
+ <h2>{t('archiveTitle')}</h2>
242
+ <span className="dut-arc-count">{rows.length} {t('archiveCount')}</span>
243
+ <span className="dut-arc-spacer" />
244
+ {rows.length > 1 ? (
245
+ <>
246
+ <button type="button" className="dut-arc-btn" disabled={busy !== null} onClick={restoreAll}>{busy === 'restore-all' ? t('archiveRestoring') : t('archiveRestoreAll')}</button>
247
+ <button type="button" className={'dut-arc-btn dut-arc-del' + (confirmAll ? ' dut-arc-confirm' : '')} disabled={busy !== null} onClick={removeAll}>{busy === 'delete-all' ? t('archiveDeleteAll') + '…' : confirmAll ? t('archiveDeleteAll') + '?' : t('archiveDeleteAll')}</button>
248
+ </>
249
+ ) : null}
250
+ </div>
251
+ {error !== null ? <div className="dut-arc-alert">{error}</div> : null}
252
+ <div className="dut-arc-body">
253
+ {rows.length === 0 ? (
254
+ <div className="dut-arc-empty">{t('archiveEmpty')}</div>
255
+ ) : rows.map(row => {
256
+ const summary = row.summary
257
+ const title = summary?.displayTitle ?? row.id
258
+ const sub = [basenameOf(summary?.cwd), summary !== undefined ? relativeTime(summary.updatedAt, now, timeFormatter) : ''].filter(Boolean).join(' · ')
259
+ return (
260
+ <div className="dut-arc-row" key={row.id}>
261
+ <div className="dut-arc-main" title={title}>
262
+ <span className="dut-arc-title">{title}</span>
263
+ {sub !== '' ? <span className="dut-arc-sub">{sub}</span> : null}
264
+ </div>
265
+ <button type="button" className="dut-arc-btn" disabled={busy !== null} onClick={() => { restore(row.id) }}>{busy === `restore:${row.id}` ? t('archiveRestoring') : t('archiveRestore')}</button>
266
+ <button
267
+ type="button"
268
+ className={'dut-arc-btn dut-arc-del' + (confirmId === row.id ? ' dut-arc-confirm' : '')}
269
+ disabled={busy !== null}
270
+ onClick={() => { remove(row.id) }}
271
+ >
272
+ {busy === `delete:${row.id}` ? t('archiveDelete') + '…' : confirmId === row.id ? t('archiveDelete') + '?' : t('archiveDelete')}
273
+ </button>
274
+ </div>
275
+ )
276
+ })}
277
+ </div>
278
+ </div>
279
+ </div>
280
+ )
281
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * ChatCode CLI visual identity.
3
+ *
4
+ * The host owns its component tree, so this module deliberately uses stable
5
+ * semantics (roles, composer data attributes and visible labels) instead of
6
+ * the host's CSS-module hashes. A small observer reapplies the few DOM
7
+ * affordances after the host swaps a session or rerenders its sidebar.
8
+ */
9
+
10
+ import type { ReactNode } from 'react'
11
+
12
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
13
+ interface SlotMap {
14
+ 'sidebar.brand.mark': {
15
+ kind: 'single'
16
+ scope: 'root'
17
+ owner: { size: number }
18
+ }
19
+ 'sidebar.brand.name': {
20
+ kind: 'single'
21
+ scope: 'root'
22
+ owner: Record<never, never>
23
+ }
24
+ }
25
+ }
26
+
27
+ /** Browser URL for the project-owned `assets/logo.svg` file. */
28
+ export const PROJECT_LOGO_URL = '/_dsh/ui-tweaks/assets/logo.svg'
29
+ const PROJECT_NAME = 'ChatCode CLI'
30
+ const LEGACY_HOST_BRAND_TEXT = /DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI/giu
31
+ const HOST_BRAND_LABEL_TEXT = /^(?:DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI)$/iu
32
+ const HOST_BUILD_VERSION_TEXT = /^v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)*$/iu
33
+ const HERO_HEADLINE = /探索未至之境/u
34
+ const HERO_PREVIEW_TEXT = /^(?:预览版|Preview)$/iu
35
+ const FAVICON_MARKER = 'data-dut-project-favicon'
36
+
37
+ /** Reusable rendering of the packaged project logo. */
38
+ export function ProjectLogoIcon({ className = '' }: { className?: string }): ReactNode {
39
+ return (
40
+ <img className={className} src={PROJECT_LOGO_URL} alt="" aria-hidden="true" />
41
+ )
42
+ }
43
+
44
+ /** New-session brand-mark slot supplied by the conversation package. */
45
+ export function HeroProjectLogo({ size, className }: { size: number, className?: string | undefined }): ReactNode {
46
+ return <img className={`${className ?? ''} dut-project-logo dut-hero-logo`} src={PROJECT_LOGO_URL} width={size} height={size} alt="" aria-hidden="true" />
47
+ }
48
+
49
+ /** Sidebar mark shared by the expanded header and the collapsed rail. */
50
+ export function SidebarProjectLogo({ size }: { size: number }): ReactNode {
51
+ return <img className="dut-project-logo dut-sidebar-logo" src={PROJECT_LOGO_URL} width={size} height={size} alt="" aria-hidden="true" />
52
+ }
53
+
54
+ /** Expanded-sidebar wordmark supplied through the host's dedicated slot. */
55
+ export function SidebarProjectName(): ReactNode {
56
+ return <span className="dut-sidebar-brand-name">ChatCode CLI</span>
57
+ }
58
+
59
+ export const BRANDING_CSS = `
60
+ /* Keep the light-theme type hierarchy in ChatCode's restrained red family,
61
+ while leaving every surface and interaction state (including workspace /
62
+ sidebar hover and selected fills, menus, the composer and shadows) owned
63
+ by the host theme. */
64
+ body:not([data-ds-dark-theme]){
65
+ --dsw-alias-label-primary:#301111;
66
+ --dsw-alias-label-secondary:#754745;
67
+ --dsw-alias-label-tertiary:#a77471;
68
+ }
69
+ /* Saturated brand red remains exclusive to explicitly marked primary actions. */
70
+ body:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}
71
+ body:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}
72
+ body:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}
73
+ /* In dark mode, alter only the resting background of primary buttons. Once
74
+ hovered, the host owns the complete interaction treatment again. */
75
+ body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}
76
+ .dut-project-logo{display:block;object-fit:contain}
77
+ .dut-left-logo{width:44px !important;height:44px !important}
78
+ .dut-brand-title{font-size:18px !important;font-weight:600 !important}
79
+ .dut-sidebar-logo{width:44px !important;height:44px !important}
80
+ /* The alpha sidebar keeps its slotted identity at 24px and clips the brand
81
+ button. Give only our expanded identity the full artwork height so the
82
+ 44px knot is not cropped by that host-owned overflow boundary. */
83
+ [class*="brandIdentity"]:has(.dut-sidebar-logo){height:44px !important}
84
+ .dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}
85
+ .dut-brand-version{display:none !important}
86
+ .dut-hero-preview{display:none !important}
87
+ .dut-hero-logo{width:60px !important;height:60px !important}
88
+ [class*="headline"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:26px !important;line-height:34px !important}
89
+ .dut-settings-logo{width:19px;height:19px;object-fit:contain}
90
+ `
91
+
92
+ function hasPrimaryLabel(button: HTMLButtonElement): boolean {
93
+ const label = `${button.textContent ?? ''} ${button.getAttribute('aria-label') ?? ''} ${button.title}`.trim().toLowerCase()
94
+ return /(?:^|\s)(new session|send|install)(?:\s|$)|新会话|发送|(?:^|\s)安装(?:\s|$)/u.test(label)
95
+ }
96
+
97
+ /** Replace one host SVG while retaining its generated layout class names. */
98
+ function replaceHostLogo(svg: SVGElement): void {
99
+ if (svg.classList.contains('dut-project-logo')) return
100
+ const image = document.createElement('img')
101
+ image.className = `${svg.getAttribute('class') ?? ''} dut-project-logo dut-left-logo`.trim()
102
+ image.src = PROJECT_LOGO_URL
103
+ image.alt = ''
104
+ image.setAttribute('aria-hidden', 'true')
105
+ const width = svg.getAttribute('width')
106
+ const height = svg.getAttribute('height')
107
+ if (width !== null) image.setAttribute('width', width)
108
+ if (height !== null) image.setAttribute('height', height)
109
+ svg.replaceWith(image)
110
+ }
111
+
112
+ /** Locate the mark adjacent to an otherwise unstructured sidebar brand label. */
113
+ function replaceLogoBesideBrandLabel(label: HTMLElement): void {
114
+ const nearby = [
115
+ label.previousElementSibling,
116
+ label.parentElement?.querySelector('svg') ?? null,
117
+ ]
118
+ for (const candidate of nearby) {
119
+ if (candidate instanceof SVGElement) {
120
+ replaceHostLogo(candidate)
121
+ return
122
+ }
123
+ const svg = candidate?.querySelector('svg')
124
+ if (svg instanceof SVGElement) {
125
+ replaceHostLogo(svg)
126
+ return
127
+ }
128
+ }
129
+ }
130
+
131
+ /** Hide the host build suffix that shares the sidebar wordmark container. */
132
+ function hideVersionBesideBrandLabel(label: HTMLElement): void {
133
+ const container = label.parentElement
134
+ if (container === null) return
135
+ for (const sibling of container.children) {
136
+ if (sibling === label || !(sibling instanceof HTMLElement)) continue
137
+ if (!HOST_BUILD_VERSION_TEXT.test(sibling.textContent?.trim() ?? '')) continue
138
+ sibling.classList.add('dut-brand-version')
139
+ sibling.setAttribute('aria-hidden', 'true')
140
+ }
141
+ }
142
+
143
+ function syncBranding(): void {
144
+ document.title = document.title.replace(LEGACY_HOST_BRAND_TEXT, PROJECT_NAME)
145
+
146
+ // The host deliberately renders the blank-session preview badge as a
147
+ // third headline column. ChatCode is the production identity here, so
148
+ // suppress that host-owned badge and collapse the headline back to two
149
+ // columns (brand mark + product name).
150
+ document.querySelectorAll<HTMLElement>('[class*="previewBadge"]').forEach((badge) => {
151
+ if (!HERO_PREVIEW_TEXT.test(badge.textContent?.trim() ?? '')) return
152
+ badge.classList.add('dut-hero-preview')
153
+ badge.setAttribute('aria-hidden', 'true')
154
+ })
155
+
156
+ // The sidebar label identifies the precise icon to its left. Process it
157
+ // before the generic text pass so the sibling relationship is still intact.
158
+ document.querySelectorAll<HTMLElement>('body *').forEach((element) => {
159
+ if (element.childElementCount !== 0 || !HOST_BRAND_LABEL_TEXT.test(element.textContent?.trim() ?? '')) return
160
+ if (element.textContent !== PROJECT_NAME) element.textContent = PROJECT_NAME
161
+ element.classList.add('dut-brand-title')
162
+ replaceLogoBesideBrandLabel(element)
163
+ hideVersionBesideBrandLabel(element)
164
+ })
165
+
166
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
167
+ let node: Text | null
168
+ while ((node = walker.nextNode() as Text | null) !== null) {
169
+ const parent = node.parentElement
170
+ if (parent?.closest('script,style') !== null) continue
171
+ if (LEGACY_HOST_BRAND_TEXT.test(node.data)) {
172
+ LEGACY_HOST_BRAND_TEXT.lastIndex = 0
173
+ node.data = node.data.replace(LEGACY_HOST_BRAND_TEXT, PROJECT_NAME)
174
+ LEGACY_HOST_BRAND_TEXT.lastIndex = 0
175
+ }
176
+ if (HERO_HEADLINE.test(node.data)) node.data = node.data.replace(HERO_HEADLINE, PROJECT_NAME)
177
+ }
178
+
179
+ // Older host builds expose a brand-mark wrapper but no text-bearing sibling.
180
+ document.querySelectorAll<HTMLElement>('[class*="brandMark"]').forEach((host) => {
181
+ const svg = host.matches('svg') ? host : host.querySelector('svg')
182
+ if (svg instanceof SVGElement) replaceHostLogo(svg)
183
+ })
184
+
185
+ document.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
186
+ button.classList.toggle('dut-unicom-primary-action', hasPrimaryLabel(button))
187
+ })
188
+ }
189
+
190
+ /** Install a last-declared SVG favicon so browsers prefer it to DSH's whale. */
191
+ function installProjectFavicon(): HTMLLinkElement {
192
+ const existing = document.head.querySelector<HTMLLinkElement>(`link[${FAVICON_MARKER}]`)
193
+ if (existing !== null) return existing
194
+ const icon = document.createElement('link')
195
+ icon.setAttribute(FAVICON_MARKER, '')
196
+ icon.rel = 'icon'
197
+ icon.type = 'image/svg+xml'
198
+ icon.href = PROJECT_LOGO_URL
199
+ document.head.appendChild(icon)
200
+ return icon
201
+ }
202
+
203
+ /** Install the brand theme and retain it across host navigation/rerenders. */
204
+ export function installBranding(): () => void {
205
+ const id = 'dsh-ui-tweaks-branding'
206
+ const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
207
+ if (existing !== null) return () => {}
208
+ const style = document.createElement('style')
209
+ style.dataset.plugin = 'dsh-ui-tweaks'
210
+ style.dataset.pluginCss = id
211
+ style.textContent = BRANDING_CSS
212
+ document.head.appendChild(style)
213
+ const favicon = installProjectFavicon()
214
+
215
+ let frame: number | undefined
216
+ const schedule = (): void => {
217
+ if (frame !== undefined) return
218
+ frame = window.requestAnimationFrame(() => {
219
+ frame = undefined
220
+ syncBranding()
221
+ })
222
+ }
223
+ syncBranding()
224
+ const observer = new MutationObserver(schedule)
225
+ observer.observe(document.body, { childList: true, characterData: true, subtree: true })
226
+ return () => {
227
+ observer.disconnect()
228
+ if (frame !== undefined) window.cancelAnimationFrame(frame)
229
+ favicon.remove()
230
+ style.remove()
231
+ }
232
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * dsh-ui-tweaks — precise cache-hit readout (browser half).
3
+ *
4
+ * Rewrites the cache-hit figure in the composer stats line under the input
5
+ * box ("缓存命中 96%" / "Cache hit 96%") to two decimal places ("96.35%").
6
+ * DSH rounds the share to an integer before display (adding decimals only to
7
+ * avoid a false 100%); the exact figure needs the raw token buckets, which
8
+ * this seat reads through the framework's per-session projection hook
9
+ * (`useProjection('tokenUsage')`) — the same durable whole-log totals the
10
+ * stock number is computed from.
11
+ *
12
+ * - Mounted in `conversation.composer.dock` (the band under the composer card
13
+ * that hosts the shipped stats line), registered only while the
14
+ * `preciseCacheHitEnabled` toggle is on — off costs nothing. The entry
15
+ * renders nothing itself: it is a live reader that patches the stock span's
16
+ * text in place, so layout, truncation and tooltip behavior stay DSH's own.
17
+ * - A MutationObserver re-applies the precise figure whenever React repaints
18
+ * the line (token updates, group churn); writes are idempotent — a rewrite
19
+ * only happens when the text actually differs — so the loop settles after
20
+ * one pass. Toggling off restores the original texts.
21
+ * @module dsh-ui-tweaks/client/cachehit
22
+ */
23
+
24
+ import { useEffect } from 'react'
25
+ import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client'
26
+
27
+ /** Client-side slice of dsh-token-meter's cumulative projection. */
28
+ interface TokenUsageProjection {
29
+ uncachedInputTokens: number
30
+ cacheReadTokens: number
31
+ cacheWriteTokens: number
32
+ }
33
+
34
+ declare module '@deepseek-ai/dsh-session-projection/types' {
35
+ interface SessionProjectionMap {
36
+ tokenUsage: TokenUsageProjection | undefined
37
+ }
38
+ }
39
+
40
+ /** Whole-span shape of the stock cache-hit group (zh + en locales). */
41
+ const CACHE_HIT_SPAN_PATTERN = /^\s*(?:缓存命中|Cache hit)\s+\d+(?:\.\d+)?%\s*$/
42
+ /** The trailing percentage inside that label — the only part rewritten. */
43
+ const PERCENT_IN_SPAN_PATTERN = /\d+(?:\.\d+)?%(?=\s*$)/
44
+
45
+ /**
46
+ * Sum the three disjoint prompt-side billing buckets — DSH's own denominator
47
+ * for the cache-hit share (`uncachedInput + cacheRead + cacheWrite`).
48
+ */
49
+ function billedInputTokens(usage: TokenUsageProjection): number {
50
+ return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens
51
+ }
52
+
53
+ export interface PreciseCacheHitEntryProps {
54
+ /** Framework seat (PropsRuntime): key-addressed per-session projection reader. */
55
+ useProjection: UseProjection
56
+ }
57
+
58
+ /** Session-scoped composer-dock seat that keeps the stock cache-hit figure at two decimals. */
59
+ export function PreciseCacheHitEntry({ useProjection }: PreciseCacheHitEntryProps) {
60
+ // The raw cumulative usage; undefined until the host's baseline/frame
61
+ // carries the key — exactly when the stock line first shows the group.
62
+ const usage = useProjection('tokenUsage')
63
+
64
+ useEffect(() => {
65
+ const bands = document.querySelectorAll<HTMLElement>('[data-slot="conversation.composer.dock"]')
66
+ if (bands.length === 0) return
67
+
68
+ /** Original texts of spans we rewrote, restored on cleanup. */
69
+ const patched = new Map<HTMLSpanElement, string>()
70
+
71
+ /**
72
+ * Two-decimal share over the raw buckets; null while there is nothing
73
+ * precise to show (projection absent, or no billed input — the stock line
74
+ * omits the group in that case too).
75
+ */
76
+ const precisePercent = (): string | null => {
77
+ if (usage === undefined) return null
78
+ const billed = billedInputTokens(usage)
79
+ if (!(billed > 0)) return null
80
+ return ((usage.cacheReadTokens / billed) * 100).toFixed(2)
81
+ }
82
+
83
+ const apply = (): void => {
84
+ const precise = precisePercent()
85
+ for (const band of bands) {
86
+ for (const el of band.querySelectorAll<HTMLSpanElement>('span')) {
87
+ const current = el.textContent ?? ''
88
+ if (!CACHE_HIT_SPAN_PATTERN.test(current)) continue
89
+ if (precise === null) {
90
+ // Projection gone (session switch race): undo any rewrite so the
91
+ // stock integer shows again instead of a stale figure.
92
+ const original = patched.get(el)
93
+ if (original !== undefined && original !== current) el.textContent = original
94
+ patched.delete(el)
95
+ continue
96
+ }
97
+ if (!patched.has(el)) patched.set(el, current)
98
+ const rewritten = current.replace(PERCENT_IN_SPAN_PATTERN, `${precise}%`)
99
+ if (rewritten !== current) el.textContent = rewritten
100
+ }
101
+ }
102
+ }
103
+
104
+ apply()
105
+ // React owns these text nodes: every repaint of the line re-triggers us,
106
+ // and our own idempotent write settles after a single extra pass.
107
+ const observer = new MutationObserver(apply)
108
+ for (const band of bands) {
109
+ observer.observe(band, { childList: true, characterData: true, subtree: true })
110
+ }
111
+ return () => {
112
+ observer.disconnect()
113
+ // Toggle-off/unmount must not leave a stale precise figure behind.
114
+ for (const [el, original] of patched) {
115
+ if ((el.textContent ?? '') !== original) el.textContent = original
116
+ }
117
+ patched.clear()
118
+ }
119
+ }, [usage])
120
+
121
+ return null
122
+ }