@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,1696 @@
1
+ /**
2
+ * dsh-ui-tweaks — browser half.
3
+ *
4
+ * Reads and writes the `ui-tweaks` settings namespace through the same-origin
5
+ * route served by the server half, applies the chosen font size / table style
6
+ * live via a runtime `<style>` element, and renders the Settings
7
+ * panel section that edits them.
8
+ */
9
+
10
+ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
11
+ import { createPortal } from 'react-dom'
12
+ import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
13
+ import type { Context as ClientContext } from '../../context-types.ts'
14
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
15
+ // Type-only import activates the dsh-client-ui-settings slot declarations
16
+ // (`settings.section`) and the client-side settings scope contract.
17
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
18
+ // Type-only import activates the dsh-client-ui-conversation slot declarations
19
+ // (`conversation.input.dock`) that host the git-bar warmup seat and the whale
20
+ // indicator, and the Context declaration for `ctx.conversation` (the /init
21
+ // command's scope-addressed send / input registry).
22
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
23
+ // Type-only imports activate the current global Session/Workspace standard
24
+ // slot props (`useSessions` / `useWorkspaces`). The removed pre-alpha
25
+ // dsh-client-runtime aggregate must not be restored by the merged UI Tweaks.
26
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client'
27
+ import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
28
+ // Type-only import activates the Context declaration for `ctx.commandUi`
29
+ // (client slash-command contributions) and provides the contribution types.
30
+ import type { SelectOption } from '@deepseek-ai/dsh-client-ui-commands/client'
31
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
32
+ import { BranchChipEntry, GitWarmup, HeaderUtilities, installGitBarStyles, installHeroChip } from './gitbar.tsx'
33
+ import { ArchiveSection, installArchiveStyles } from './archive.tsx'
34
+ import { McpSection, installMcpStyles } from './mcp.tsx'
35
+ import { WhaleIndicator, installWhaleStyles } from './whale.tsx'
36
+ import { PreciseCacheHitEntry } from './cachehit.tsx'
37
+ import { installTaskNotifier, previewAlerts, requestNotifyPermission } from './notifier.ts'
38
+ import { HeroProjectLogo, installBranding, ProjectLogoIcon, SidebarProjectLogo, SidebarProjectName } from './branding.tsx'
39
+
40
+ const NS = 'ui-tweaks'
41
+ const SETTINGS_ROUTE = '/_dsh/ui-tweaks/settings'
42
+
43
+ /** Client context narrowed to the browser sessions face in this combined build. */
44
+ type UITweaksClientContext = ClientContext & { sessions: ISessions }
45
+
46
+ const DEFAULT_FONT_SIZE = 16
47
+ const MIN_FONT_SIZE = 10
48
+ const MAX_FONT_SIZE = 32
49
+ /** Legacy: code font as a percentage of the message font size (81% = stock 13/16). */
50
+ const DEFAULT_CODE_FONT_SCALE = 81
51
+ /** Absolute code font size (px): 13 matches the host default at a 16px body. */
52
+ const DEFAULT_CODE_FONT_SIZE = 13
53
+ const MIN_CODE_FONT_SIZE = 8
54
+ const MAX_CODE_FONT_SIZE = 32
55
+ /** 行高 base unit (px): message/block gaps, text line-height, paragraph & list margins all scale from it. */
56
+ const DEFAULT_LINE_HEIGHT = 16
57
+ const MIN_LINE_HEIGHT = 0
58
+ const MAX_LINE_HEIGHT = 64
59
+
60
+ interface TweaksValue {
61
+ fontSize?: number
62
+ /** Code font size as a percentage of the message font size (81 = stock). Legacy input. */
63
+ codeFontScale?: number
64
+ /** Absolute code font size in px; wins over the legacy percentage. */
65
+ codeFontSize?: number
66
+ /** 行高 base unit in px; scales message/block gaps, line-height and list/paragraph margins. */
67
+ lineHeight?: number
68
+ tableStyle?: 'default' | 'claude'
69
+ /** Whether the GitBar (branch / diff / commit pills) is shown. */
70
+ gitBarEnabled?: boolean
71
+ /** Whether the Archive manager (sidebar entry above Settings) is shown. */
72
+ archiveManagerEnabled?: boolean
73
+ /** Whether the MCP manager (Settings page listing MCP servers) is shown. */
74
+ mcpManagerEnabled?: boolean
75
+ /** Whether the /init slash command (AGENTS.md bootstrap prompt) is registered. */
76
+ initCommandEnabled?: boolean
77
+ /** Whether the whale working indicator above the input is shown. */
78
+ whaleIndicatorEnabled?: boolean
79
+ /** Whether the stats-line cache-hit figure keeps two decimals. Keep in sync with src/config.ts. */
80
+ preciseCacheHitEnabled?: boolean
81
+ /** Whether task notifications are active. Keep in sync with src/config.ts. */
82
+ notificationsEnabled?: boolean
83
+ /** Alert only while the tab is hidden or unfocused. */
84
+ notifyOnlyWhenHidden?: boolean
85
+ /** Alert when a session completes its turn. */
86
+ notifyOnComplete?: boolean
87
+ /** Alert when a session blocks on an approval, plan review or question. */
88
+ notifyOnInteraction?: boolean
89
+ /** Blink an unread counter into the tab title. */
90
+ notifyTitleFlash?: boolean
91
+ /** Fire desktop-level Web Notifications. */
92
+ notifySystemNotification?: boolean
93
+ /** Play the synthesized two-note chime. */
94
+ notifySound?: boolean
95
+ }
96
+
97
+ interface ResolvedTweaks {
98
+ fontSize: number
99
+ /** Effective absolute code font size in px (codeFontSize, else legacy %, else stock). */
100
+ codeFontSize: number
101
+ lineHeight: number
102
+ tableStyle: 'default' | 'claude'
103
+ gitBarEnabled: boolean
104
+ archiveManagerEnabled: boolean
105
+ mcpManagerEnabled: boolean
106
+ initCommandEnabled: boolean
107
+ whaleIndicatorEnabled: boolean
108
+ preciseCacheHitEnabled: boolean
109
+ notificationsEnabled: boolean
110
+ notifyOnlyWhenHidden: boolean
111
+ notifyOnComplete: boolean
112
+ notifyOnInteraction: boolean
113
+ notifyTitleFlash: boolean
114
+ notifySystemNotification: boolean
115
+ notifySound: boolean
116
+ }
117
+
118
+ interface UITweaksSnapshot {
119
+ writable: boolean
120
+ value: TweaksValue
121
+ revision: number
122
+ }
123
+
124
+ interface ApiSuccess<T> { ok: true; value: T }
125
+ interface ApiFailure { ok: false; error: { code: string; message: string } }
126
+
127
+ const en = {
128
+ nav: 'UI Tweaks',
129
+ settingsTitle: 'UI Tweaks',
130
+ settingsIntro: 'Tune the conversation UI — text, tables and layout, plus optional features: git bar, archive & MCP managers and the /init command. Changes apply live.',
131
+ sectionText: 'Text & tables',
132
+ sectionLayout: 'Layout',
133
+ sectionFeatures: 'Features',
134
+ fontSize: 'Message font size',
135
+ fontSizeHint: `Number between ${MIN_FONT_SIZE} and ${MAX_FONT_SIZE}; applies to message text, headings, tables and code.`,
136
+ codeFontSize: 'Code font size',
137
+ codeFontSizeHint: `Absolute code size in px (${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}); ${DEFAULT_CODE_FONT_SIZE}px is ChatCode CLI's default at a 16px body. Applies to code blocks; inline code follows proportionally.`,
138
+ lineHeight: 'Line spacing',
139
+ lineHeightHint: `Base vertical spacing in px: gaps between message rows (Think ↔ tool cards) and blocks inside one reply, plus text line-height, paragraph and list margins, all scale from it; ${DEFAULT_LINE_HEIGHT} is ChatCode CLI's default.`,
140
+ tableStyle: 'Table style',
141
+ tableStyleHint: 'Cell look for markdown tables: stock borders, or the Claude Desktop card style.',
142
+ tableStyleDefault: 'Default',
143
+ tableStyleClaude: 'Claude Desktop',
144
+ gitBar: 'Git bar',
145
+ gitBarHint: 'Branch / diff pills above the input inside git repos, with branch management and commit & push; auto-hidden outside git.',
146
+ gitBarOn: 'On',
147
+ gitBarOff: 'Off',
148
+ archiveManager: 'Archive manager',
149
+ archiveManagerHint: 'An Archive settings page to restore or permanently delete archived sessions.',
150
+ archiveManagerOn: 'On',
151
+ archiveManagerOff: 'Off',
152
+ mcpManager: 'MCP manager',
153
+ mcpManagerHint: 'An MCP settings page listing servers with status and tools; edit and restart them.',
154
+ mcpManagerOn: 'On',
155
+ mcpManagerOff: 'Off',
156
+ mcpNav: 'MCP',
157
+ mcpTitle: 'MCP servers',
158
+ mcpEmpty: 'No MCP servers configured.',
159
+ mcpStatusActive: 'Running',
160
+ mcpStatusFailed: 'Failed',
161
+ mcpStatusLoading: 'Loading',
162
+ mcpStatusStopped: 'Stopped',
163
+ mcpStatusDisabled: 'Disabled',
164
+ mcpTools: 'tools',
165
+ mcpEnv: 'Env',
166
+ mcpAdd: 'Add server',
167
+ mcpAddTitle: 'Add MCP server',
168
+ mcpEditTitle: 'Edit MCP server',
169
+ mcpEdit: 'Edit',
170
+ mcpDelete: 'Delete',
171
+ mcpEnabledAction: 'Enable',
172
+ mcpDisabledAction: 'Disable',
173
+ mcpSaved: 'Saved.',
174
+ mcpRemoved: 'Removed.',
175
+ mcpFormTab: 'Form',
176
+ mcpYamlTab: 'YAML',
177
+ mcpYamlHint: 'Fill in the MCP config directly (serverName / transport / command / args / env / toolCallTimeoutMs / url / headers …). It is validated before saving.',
178
+ mcpYamlPlaceholder: 'serverName: my-server\ntransport: stdio\ncommand: npx\nargs:\n - "-y"\n - "@some/mcp-server"\nenv:\n KEY: value\ntoolCallTimeoutMs: 60000',
179
+ mcpFieldId: 'Instance ID',
180
+ mcpFieldIdHint: 'Loader entry id (letters, digits, - and _ only); it must be unique.',
181
+ mcpFieldName: 'Name',
182
+ mcpFieldNameHint: 'Tool namespace `mcp__<name>__*`; letters, digits, - and _ (1–32).',
183
+ mcpFieldType: 'Type',
184
+ mcpFieldTimeout: 'Timeout (ms)',
185
+ mcpFieldCommand: 'Command',
186
+ mcpFieldCommandPlaceholder: 'e.g. npx',
187
+ mcpFieldArgs: 'Arguments (one per line)',
188
+ mcpFieldEnv: 'Environment vars (optional, KEY=value per line)',
189
+ mcpFieldUrl: 'URL',
190
+ mcpFieldHeaders: 'Headers (optional, Key: value per line)',
191
+ mcpFieldEnabled: 'Enabled',
192
+ mcpSave: 'Save',
193
+ mcpCancel: 'Cancel',
194
+ mcpInvalidId: 'Instance ID may only contain letters, digits, - and _.',
195
+ mcpInvalidName: 'Name may only contain letters, digits, - and _ (1–32 chars).',
196
+ mcpInvalidTimeout: 'Timeout must be a positive integer (ms).',
197
+ mcpInvalidUrl: 'URL must start with http:// or https://.',
198
+ mcpInvalidCommand: 'Command is required.',
199
+ mcpUnavailable: 'MCP manager unavailable.',
200
+ mcpDisabledHint: 'MCP management is off. Turn it on in 界面调整 (UI Tweaks) to view and restart MCP servers here.',
201
+ mcpEnable: 'Enable MCP manager',
202
+ initCommand: '/init command',
203
+ initCommandHint: 'The /init slash command: pick a prompt language and ChatCode CLI analyzes the project and writes AGENTS.md.',
204
+ initCommandOn: 'On',
205
+ initCommandOff: 'Off',
206
+ whaleIndicator: 'Whale indicator',
207
+ whaleIndicatorHint: 'A little whale above the input box: translucent while idle; while the model works it swims and breathes blue.',
208
+ whaleIndicatorOn: 'On',
209
+ whaleIndicatorOff: 'Off',
210
+ preciseCacheHit: 'Precise cache hit',
211
+ preciseCacheHitHint: 'Rewrites the cache-hit figure in the stats line under the input box to two decimal places (e.g. 96.35%), computed from the raw cached-read / cached-write / uncached-input token buckets instead of the stock rounded integer.',
212
+ preciseCacheHitOn: 'On',
213
+ preciseCacheHitOff: 'Off',
214
+ sectionNotifications: 'Task alerts',
215
+ notifications: 'Enable alerts',
216
+ notificationsHint: 'Call you back while the tab is in the background: a session finishes its turn, or one starts waiting for your approval, plan review or answer. The switches below apply only while this is on.',
217
+ notifyOn: 'On',
218
+ notifyOff: 'Off',
219
+ notifyOnComplete: 'Alert on finish',
220
+ notifyOnCompleteHint: 'Fire when a session ends its turn — completed, interrupted or failed; the notification says which.',
221
+ notifyOnInteraction: 'Alert on interaction',
222
+ notifyOnInteractionHint: 'Fire when a session waits on you: an approval, a plan review, or a question from the agent.',
223
+ notifyOnlyWhenHidden: 'Only when hidden',
224
+ notifyOnlyWhenHiddenHint: 'Stay quiet while you are looking at this page; alert only once the tab is hidden or unfocused.',
225
+ notifyTitleFlash: 'Tab title flash',
226
+ notifyTitleFlashHint: 'Blink an unread counter into the tab title until you come back.',
227
+ notifySystemNotification: 'System notifications',
228
+ notifySystemNotificationHint: 'Desktop-level notifications; click one to jump straight to that session. Permission is requested when enabled.',
229
+ notifySound: 'Chime',
230
+ notifySoundHint: 'A soft two-note motif — rising when work finishes, falling when it needs you.',
231
+ notifyTest: 'Test',
232
+ notifyTitleDone: 'Task finished',
233
+ notifyTitleAborted: 'Task interrupted',
234
+ notifyTitleFailed: 'Request failed',
235
+ notifyTitlePending: 'Needs you',
236
+ bodyComplete: '✅ {title} finished its turn.',
237
+ bodyAborted: '⏹ {title} was interrupted before finishing.',
238
+ bodyFailed: '❌ {title} failed: {error}',
239
+ bodyApproval: '⏸ {title} is waiting for your approval.',
240
+ bodyPlan: '📋 {title} has a plan awaiting your review.',
241
+ bodyQuestion: '❓ {title} asked you a question.',
242
+ mcpServerDetail: 'Configured in the profile cordis.patch.yml as @deepseek-ai/dsh-mcp-client instances; add / edit / disable / delete write to that file and apply live.',
243
+ archiveNav: 'Archive',
244
+ archiveTitle: 'Archived sessions',
245
+ archiveEmpty: 'No archived sessions.',
246
+ archiveRestore: 'Restore',
247
+ archiveRestoring: 'Working…',
248
+ archiveDelete: 'Delete',
249
+ archiveDeleteAll: 'Delete all',
250
+ archiveRestoreAll: 'Restore all',
251
+ archiveCount: 'sessions',
252
+ archiveUnavailable: 'Archive unavailable.',
253
+ archiveLiveError: 'This session is running; close it before permanently deleting it.',
254
+ archiveDisabledHint: 'Archive management is off. Turn it on in 界面调整 (UI Tweaks) to restore or permanently delete archived sessions here.',
255
+ archiveEnable: 'Enable archive management',
256
+ archiveRestored: 'Restored.',
257
+ defaultAction: 'Default',
258
+ reset: 'Reset',
259
+ resetDone: 'Reset to default.',
260
+ applied: 'Applied',
261
+ unavailable: 'Settings unavailable.',
262
+ loading: 'Loading…',
263
+ readOnly: 'The active Settings provider is read-only.',
264
+ saved: 'Saved.',
265
+ commitMessage: 'Commit',
266
+ diffFiles: 'files',
267
+ branchLocal: 'Local branches',
268
+ branchRemote: 'Remote branches',
269
+ branchNew: 'New branch',
270
+ branchNewPlaceholder: 'Branch name, e.g. fix/typo',
271
+ branchCreate: 'Create',
272
+ diffTitle: 'Changes',
273
+ diffOnly: 'Hunks',
274
+ diffFull: 'Full file',
275
+ commitTitle: 'Commit changes',
276
+ commitPlaceholder: 'Describe your changes…',
277
+ commitHint: 'Committing stages the checked files (untracked included) and commits; unchecked files are excluded from this commit. “Commit & push” also pushes; a new branch gets an -u upstream.',
278
+ commitWillCommit: 'To commit',
279
+ commitViewDiff: 'View',
280
+ commitEmpty: 'Write a commit message first',
281
+ commitCancel: 'Cancel',
282
+ commitSubmit: 'Commit',
283
+ commitSubmitPush: 'Commit & push',
284
+ commitBusy: 'Task in progress — commit is unavailable while the agent is working.',
285
+ dirty: 'Uncommitted changes',
286
+ clean: 'Working tree clean',
287
+ noChanges: 'No changes here.',
288
+ branchDelete: 'Delete branch',
289
+ branchDeleteConfirm: 'Confirm?',
290
+ branchRemoteDelete: 'Delete remote branch',
291
+ branchFrom: 'From branch',
292
+ branchFromHead: 'Current HEAD (default)',
293
+ branchGraph: 'Graph',
294
+ branchCancel: 'Cancel',
295
+ branchRefresh: 'Refresh',
296
+ graphTitle: 'Commit graph',
297
+ graphColGraph: 'Graph',
298
+ graphColCommit: 'Commit',
299
+ graphColSubject: 'Description',
300
+ graphColAuthor: 'Author',
301
+ graphColDate: 'Date',
302
+ branchPushRemote: 'Push to remote',
303
+ includeFile: 'Include in commit',
304
+ excludeFile: 'Exclude from commit',
305
+ openProject: 'Open project',
306
+ terminal: 'Terminal',
307
+ openExplorer: 'Explorer',
308
+ openVscode: 'VS Code',
309
+ openIdea: 'IntelliJ IDEA',
310
+ openGoland: 'GoLand',
311
+ openWebstorm: 'WebStorm',
312
+ openPycharm: 'PyCharm',
313
+ termConnecting: 'connecting…',
314
+ termExited: 'shell exited',
315
+ termUnavailable: 'PTY unavailable: node-pty failed to load on the host.',
316
+ termLost: 'connection lost',
317
+ initDesc: 'Analyze this project and generate an AGENTS.md for future coding agents',
318
+ initOptionZh: 'AGENTS.md — Chinese prompt',
319
+ initOptionZhDetail: 'Submit a Chinese prompt asking the agent to analyze the project and write or improve AGENTS.md.',
320
+ initOptionEn: 'AGENTS.md — English prompt',
321
+ initOptionEnDetail: 'Submit an English prompt asking the agent to analyze the project and write or improve AGENTS.md.',
322
+ initFailed: '/init failed to send the prompt',
323
+ } as const
324
+
325
+ type LocaleKey = keyof typeof en
326
+
327
+ const zh: Record<LocaleKey, string> = {
328
+ nav: '界面调整',
329
+ settingsTitle: '界面调整',
330
+ settingsIntro: '调整对话界面——文本、表格与布局,以及 Git 状态栏、归档 / MCP 管理、/init 命令等功能开关,修改即时生效。',
331
+ sectionText: '文本与表格',
332
+ sectionLayout: '布局',
333
+ sectionFeatures: '功能',
334
+ fontSize: '消息字体大小',
335
+ fontSizeHint: `取值 ${MIN_FONT_SIZE}–${MAX_FONT_SIZE},作用于消息正文、标题、表格与代码。`,
336
+ codeFontSize: '代码字号',
337
+ codeFontSizeHint: `代码绝对字号,取值 ${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}px;${DEFAULT_CODE_FONT_SIZE}px 为 ChatCode CLI 默认(正文 16 时)。作用于代码块,行内代码按比例跟随。`,
338
+ lineHeight: '行高',
339
+ lineHeightHint: `回复区垂直间距的基准值:消息之间(如 Think ↔ 工具卡片)、回复内块之间、正文行高、段落与列表边距都按它缩放;取值 ${MIN_LINE_HEIGHT}–${MAX_LINE_HEIGHT}px,${DEFAULT_LINE_HEIGHT} 为 ChatCode CLI 默认。`,
340
+ tableStyle: '表格样式',
341
+ tableStyleHint: 'Markdown 表格的外观:默认边框,或 Claude Desktop 卡片风格。',
342
+ tableStyleDefault: '默认',
343
+ tableStyleClaude: 'Claude Desktop',
344
+ gitBar: 'Git 状态栏',
345
+ gitBarHint: 'git 仓库内时在输入框上方显示 分支 / 差异 胶囊,支持分支管理与提交推送;非 git 目录自动隐藏。',
346
+ gitBarOn: '开启',
347
+ gitBarOff: '关闭',
348
+ archiveManager: '归档管理',
349
+ archiveManagerHint: '在设置中显示「归档」页面:恢复或彻底删除已归档会话。',
350
+ archiveManagerOn: '开启',
351
+ archiveManagerOff: '关闭',
352
+ mcpManager: 'MCP 管理',
353
+ mcpManagerHint: '在设置中显示「MCP 管理」页面:查看服务器状态与工具,可编辑并重启。',
354
+ mcpManagerOn: '开启',
355
+ mcpManagerOff: '关闭',
356
+ mcpNav: 'MCP 管理',
357
+ mcpTitle: 'MCP 服务器',
358
+ mcpEmpty: '未配置任何 MCP 服务器。',
359
+ mcpStatusActive: '运行中',
360
+ mcpStatusFailed: '错误',
361
+ mcpStatusLoading: '加载中',
362
+ mcpStatusStopped: '未运行',
363
+ mcpStatusDisabled: '已停用',
364
+ mcpTools: '个工具',
365
+ mcpEnv: '环境变量',
366
+ mcpAdd: '添加服务器',
367
+ mcpAddTitle: '添加 MCP 服务器',
368
+ mcpEditTitle: '编辑 MCP 服务器',
369
+ mcpEdit: '编辑',
370
+ mcpDelete: '删除',
371
+ mcpEnabledAction: '启用',
372
+ mcpDisabledAction: '停用',
373
+ mcpSaved: '已保存。',
374
+ mcpRemoved: '已删除。',
375
+ mcpFormTab: '表单',
376
+ mcpYamlTab: 'YAML',
377
+ mcpYamlHint: '直接填写 MCP 配置(serverName / transport / command / args / env / toolCallTimeoutMs / url / headers …),保存前会校验格式。',
378
+ mcpYamlPlaceholder: 'serverName: my-server\ntransport: stdio\ncommand: npx\nargs:\n - "-y"\n - "@some/mcp-server"\nenv:\n KEY: value\ntoolCallTimeoutMs: 60000',
379
+ mcpFieldId: '实例 ID',
380
+ mcpFieldIdHint: '加载器条目 ID(仅字母、数字、- 和 _),需唯一。',
381
+ mcpFieldName: '名称',
382
+ mcpFieldNameHint: '工具命名空间 `mcp__<名称>__*`;字母、数字、- 和 _(1–32 字符)。',
383
+ mcpFieldType: '类型',
384
+ mcpFieldTimeout: '超时时间 (ms)',
385
+ mcpFieldCommand: '命令',
386
+ mcpFieldCommandPlaceholder: '如 npx',
387
+ mcpFieldArgs: '参数(每行一个)',
388
+ mcpFieldEnv: '环境变量(可选,每行 KEY=值)',
389
+ mcpFieldUrl: 'URL',
390
+ mcpFieldHeaders: '请求头(可选,每行 Key: 值)',
391
+ mcpFieldEnabled: '启用',
392
+ mcpSave: '保存',
393
+ mcpCancel: '取消',
394
+ mcpInvalidId: '实例 ID 只能包含字母、数字、- 和 _。',
395
+ mcpInvalidName: '名称只能包含字母、数字、- 和 _(1–32 字符)。',
396
+ mcpInvalidTimeout: '超时时间必须是正整数(毫秒)。',
397
+ mcpInvalidUrl: 'URL 必须以 http:// 或 https:// 开头。',
398
+ mcpInvalidCommand: '命令不能为空。',
399
+ mcpUnavailable: 'MCP 管理暂不可用。',
400
+ mcpDisabledHint: 'MCP 管理尚未开启。在「界面调整」中开启“MCP 管理”后,可在此查看并重启 MCP 服务器。',
401
+ mcpEnable: '开启 MCP 管理',
402
+ initCommand: '/init 命令',
403
+ initCommandHint: '注册 /init 斜杠命令:选择提示词语言后,让 ChatCode CLI 分析项目并生成 AGENTS.md。',
404
+ initCommandOn: '开启',
405
+ initCommandOff: '关闭',
406
+ whaleIndicator: '鲸鱼指示器',
407
+ whaleIndicatorHint: '输入框上方的小鲸鱼:空闲时半透明静止,模型工作时游动并在蓝黑之间呼吸变幻。',
408
+ whaleIndicatorOn: '开启',
409
+ whaleIndicatorOff: '关闭',
410
+ preciseCacheHit: '缓存命中率两位小数',
411
+ preciseCacheHitHint: '把输入框下方统计条里的缓存命中百分比改写为两位小数(如 96.35%)——用原始 token 数(缓存读取 ÷ 计费输入)计算,而不是宿主取整后的整数。',
412
+ preciseCacheHitOn: '开启',
413
+ preciseCacheHitOff: '关闭',
414
+ sectionNotifications: '任务提醒',
415
+ notifications: '启用提醒',
416
+ notificationsHint: '标签页在后台时唤你回来:会话完成了任务,或开始等待你的审批、计划确认或回答。下方开关仅在总开关开启时生效。',
417
+ notifyOn: '开启',
418
+ notifyOff: '关闭',
419
+ notifyOnComplete: '完成提醒',
420
+ notifyOnCompleteHint: '会话结束一轮任务时提醒——完成、被中断或出错都会通知,并注明是哪种情况。',
421
+ notifyOnInteraction: '交互提醒',
422
+ notifyOnInteractionHint: '会话等待你操作时提醒:审批、计划确认,或模型向你提问。',
423
+ notifyOnlyWhenHidden: '仅页面不可见时',
424
+ notifyOnlyWhenHiddenHint: '你正盯着本页时保持安静;切走标签页或最小化窗口后才开始提醒。',
425
+ notifyTitleFlash: '标题闪烁',
426
+ notifyTitleFlashHint: '在浏览器标签页标题中闪烁未读计数,直到你回到页面。',
427
+ notifySystemNotification: '系统通知',
428
+ notifySystemNotificationHint: '桌面级通知;点击通知可直达对应会话。开启时会向浏览器申请通知权限。',
429
+ notifySound: '提示音',
430
+ notifySoundHint: '轻柔的双音提示——上行表示完成,下行表示需要你处理。',
431
+ notifyTest: '测试',
432
+ notifyTitleDone: '任务完成',
433
+ notifyTitleAborted: '任务已中断',
434
+ notifyTitleFailed: '请求失败',
435
+ notifyTitlePending: '需要你处理',
436
+ bodyComplete: '✅ 「{title}」的任务已完成。',
437
+ bodyAborted: '⏹ 「{title}」的任务被中断了。',
438
+ bodyFailed: '❌ 「{title}」的任务出错了:{error}',
439
+ bodyApproval: '⏸ 「{title}」正在等待你的审批。',
440
+ bodyPlan: '📋 「{title}」有计划待确认。',
441
+ bodyQuestion: '❓ 「{title}」向你提问了。',
442
+ mcpServerDetail: 'MCP 服务器配置在 profile 的 cordis.patch.yml(@deepseek-ai/dsh-mcp-client 实例);添加 / 编辑 / 停用 / 删除会写入该文件,改动实时生效。',
443
+ archiveNav: '归档',
444
+ archiveTitle: '已归档会话',
445
+ archiveEmpty: '暂无归档会话。',
446
+ archiveRestore: '恢复',
447
+ archiveRestoring: '处理中…',
448
+ archiveDelete: '删除',
449
+ archiveDeleteAll: '全部删除',
450
+ archiveRestoreAll: '全部恢复',
451
+ archiveCount: '个会话',
452
+ archiveUnavailable: '归档暂不可用。',
453
+ archiveLiveError: '该会话正在运行,无法彻底删除,请先关闭该会话。',
454
+ archiveDisabledHint: '归档管理尚未开启。在「界面调整」中开启“归档管理”后,可在此查看、恢复或彻底删除已归档会话。',
455
+ archiveEnable: '开启归档管理',
456
+ archiveRestored: '已恢复。',
457
+ defaultAction: '默认',
458
+ reset: '重置',
459
+ resetDone: '已重置为默认。',
460
+ applied: '已应用',
461
+ unavailable: '设置暂不可用。',
462
+ loading: '加载中…',
463
+ readOnly: '当前设置提供方为只读。',
464
+ saved: '已保存。',
465
+ commitMessage: 'Commit',
466
+ diffFiles: '个文件',
467
+ branchLocal: '本地分支',
468
+ branchRemote: '远程分支',
469
+ branchNew: '新建分支',
470
+ branchNewPlaceholder: '分支名,如 fix/typo',
471
+ branchCreate: '创建',
472
+ diffTitle: '变更',
473
+ diffOnly: '仅差异',
474
+ diffFull: '完整文件',
475
+ commitTitle: '提交变更',
476
+ commitPlaceholder: '描述你的改动…',
477
+ commitHint: '提交会暂存勾选的文件(含未跟踪新文件);取消勾选的文件将不包含在本次提交中。「提交并推送」提交后自动 push,新分支自动 -u 设上游。',
478
+ commitWillCommit: '将提交',
479
+ commitViewDiff: '查看',
480
+ commitEmpty: '请先填写提交说明',
481
+ commitCancel: '取消',
482
+ commitSubmit: '提交',
483
+ commitSubmitPush: '提交并推送',
484
+ commitBusy: '任务进行中,暂不能提交。',
485
+ dirty: '有未提交改动',
486
+ clean: '工作区干净',
487
+ noChanges: '这里没有差异。',
488
+ branchDelete: '删除分支',
489
+ branchDeleteConfirm: '确认删除?',
490
+ branchRemoteDelete: '删除远程分支',
491
+ branchFrom: '基于分支',
492
+ branchFromHead: '当前 HEAD(默认)',
493
+ branchGraph: '图谱',
494
+ branchCancel: '取消',
495
+ branchRefresh: '刷新',
496
+ graphTitle: '提交图谱',
497
+ graphColGraph: '图',
498
+ graphColCommit: '提交',
499
+ graphColSubject: '描述',
500
+ graphColAuthor: '作者',
501
+ graphColDate: '日期',
502
+ branchPushRemote: '推送到远程',
503
+ includeFile: '提交包含此文件',
504
+ excludeFile: '提交排除此文件',
505
+ openProject: '打开项目',
506
+ terminal: '终端',
507
+ openExplorer: '资源管理器',
508
+ openVscode: 'VS Code',
509
+ openIdea: 'IntelliJ IDEA',
510
+ openGoland: 'GoLand',
511
+ openWebstorm: 'WebStorm',
512
+ openPycharm: 'PyCharm',
513
+ termConnecting: '连接中…',
514
+ termExited: 'shell 已退出',
515
+ termUnavailable: '终端不可用:宿主加载 node-pty 失败,无法启动 PTY 会话。',
516
+ termLost: '连接已断开',
517
+ initDesc: '分析当前项目并生成 AGENTS.md,供未来的 AI 编码代理使用',
518
+ initOptionZh: 'AGENTS.md(中文提示词)',
519
+ initOptionZhDetail: '向会话提交中文提示词,让代理分析项目并生成或改进 AGENTS.md。',
520
+ initOptionEn: 'AGENTS.md(英文提示词)',
521
+ initOptionEnDetail: '向会话提交英文提示词,让代理分析项目并生成或改进 AGENTS.md。',
522
+ initFailed: '/init 提示词发送失败',
523
+ }
524
+
525
+ type Translate = (key: LocaleKey) => string
526
+
527
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
528
+ interface LocaleNamespaceMap {
529
+ /** dsh-ui-tweaks Settings copy. */
530
+ 'ui-tweaks': LocaleKey
531
+ }
532
+ }
533
+
534
+ function resolveValue(value: TweaksValue | undefined): ResolvedTweaks {
535
+ const fontSize = typeof value?.fontSize === 'number' ? value.fontSize : DEFAULT_FONT_SIZE
536
+ // Effective code size: the absolute px input wins; otherwise derive px from
537
+ // the legacy percentage at the resolved body size; otherwise stock.
538
+ const codeFontSize = typeof value?.codeFontSize === 'number'
539
+ ? Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, value.codeFontSize))
540
+ : Math.max(8, Math.round(fontSize * (13 / 16) * ((value?.codeFontScale ?? DEFAULT_CODE_FONT_SCALE) / DEFAULT_CODE_FONT_SCALE)))
541
+ return {
542
+ fontSize,
543
+ codeFontSize,
544
+ lineHeight: typeof value?.lineHeight === 'number' ? value.lineHeight : DEFAULT_LINE_HEIGHT,
545
+ tableStyle: value?.tableStyle === 'claude' ? 'claude' : 'default',
546
+ gitBarEnabled: value?.gitBarEnabled ?? false,
547
+ archiveManagerEnabled: value?.archiveManagerEnabled ?? false,
548
+ mcpManagerEnabled: value?.mcpManagerEnabled ?? false,
549
+ initCommandEnabled: value?.initCommandEnabled ?? false,
550
+ whaleIndicatorEnabled: value?.whaleIndicatorEnabled ?? false,
551
+ preciseCacheHitEnabled: value?.preciseCacheHitEnabled ?? false,
552
+ notificationsEnabled: value?.notificationsEnabled ?? false,
553
+ notifyOnlyWhenHidden: value?.notifyOnlyWhenHidden ?? true,
554
+ notifyOnComplete: value?.notifyOnComplete ?? true,
555
+ notifyOnInteraction: value?.notifyOnInteraction ?? true,
556
+ notifyTitleFlash: value?.notifyTitleFlash ?? true,
557
+ notifySystemNotification: value?.notifySystemNotification ?? true,
558
+ notifySound: value?.notifySound ?? false,
559
+ }
560
+ }
561
+
562
+ /** Scale one theme px value proportionally to the chosen base size. */
563
+ function rel(base: number, fontSize: number): number {
564
+ return Math.max(8, Math.round((base / DEFAULT_FONT_SIZE) * fontSize))
565
+ }
566
+
567
+ /** Rebuild the markdown font tokens for the chosen base size, keeping the theme faces. */
568
+ function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number): string {
569
+ const cs = getComputedStyle(document.body)
570
+ const fam = (name: string, fallback: string): string => {
571
+ const value = cs.getPropertyValue(name).trim()
572
+ return value.length > 0 ? value : fallback
573
+ }
574
+ const base = fam('--dsw-font-markdown-base-font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif')
575
+ const code = fam('--dsw-font-markdown-code-font-family', '"SF Mono", Consolas, monospace')
576
+ const codeBlock = fam('--dsw-font-markdown-code-block-font-family', '"SF Mono", Consolas, monospace')
577
+
578
+ const parts: string[] = []
579
+ // `baseLine` is the token's line-height at the 16px baseline; it scales with
580
+ // both the chosen font size and the 行高 unit, so at the default (16) it
581
+ // reproduces the stock rhythm exactly.
582
+ const token = (shorthand: string, size: number, baseLine: number, family: string): void => {
583
+ const line = Math.max(12, Math.round((baseLine / DEFAULT_FONT_SIZE) * fontSize * (lineHeight / DEFAULT_LINE_HEIGHT)))
584
+ parts.push(`--${shorthand}:${size}px/${line}px ${family}`)
585
+ parts.push(`--${shorthand}-font-size:${size}px`)
586
+ parts.push(`--${shorthand}-line-height:${line}px`)
587
+ }
588
+ // Code fonts hang off the absolute code-block size (stock: 13px at a 16px
589
+ // body), with inline code slightly larger and the small variant slightly
590
+ // smaller, preserving DSH's hierarchy (14/13 and 12/13 of the block).
591
+ const codePx = (blockRatio: number): number => Math.max(8, Math.round(codeFontSize * blockRatio))
592
+ token('dsw-font-markdown-base', fontSize, 28, base)
593
+ token('dsw-font-markdown-base-strong', fontSize, 28, base)
594
+ token('dsw-font-markdown-base-italic', fontSize, 28, base)
595
+ token('dsw-font-markdown-base-strong-italic', fontSize, 28, base)
596
+ token('dsw-font-markdown-h1', rel(24, fontSize), 34, base)
597
+ token('dsw-font-markdown-h2', rel(22, fontSize), 32, base)
598
+ token('dsw-font-markdown-h3', rel(20, fontSize), 30, base)
599
+ token('dsw-font-markdown-h4', fontSize, 28, base)
600
+ token('dsw-font-markdown-code', codePx(14 / 13), 22, code)
601
+ token('dsw-font-markdown-code-block', codePx(1), 22, codeBlock)
602
+ token('dsw-font-markdown-code-block-small', codePx(12 / 13), 18, codeBlock)
603
+ return `body{${parts.join(';')}}`
604
+ }
605
+
606
+ /**
607
+ * Claude Desktop-ish markdown table look: light-gray rounded cell cards with
608
+ * small gaps, no borders. Cells share the theme's inline-code background and
609
+ * follow the message font-size setting. Alignment is left to the markdown
610
+ * renderer, so headers and cells always match.
611
+ */
612
+ const CLAUDE_TABLE_CSS = `
613
+ div[data-slot="conversation.chat.node"] table{
614
+ border-collapse:separate !important;
615
+ border-spacing:3px !important;
616
+ width:100% !important;
617
+ /* Fill the column like Claude Desktop, but never squeeze below the natural
618
+ (no-wrap) width: a table too wide to fit keeps its real width and scrolls
619
+ inside the stock table wrapper instead of hiding columns. */
620
+ min-width:max-content !important;
621
+ border:none !important;
622
+ font-size:var(--dsw-font-markdown-base-font-size) !important;
623
+ }
624
+ div[data-slot="conversation.chat.node"] table thead th{
625
+ background:var(--dsw-alias-markdown-inline-code) !important;
626
+ color:inherit !important;
627
+ font-weight:400 !important;
628
+ font-size:inherit !important;
629
+ padding:7px 10px !important;
630
+ border:none !important;
631
+ border-radius:6px !important;
632
+ }
633
+ div[data-slot="conversation.chat.node"] table tbody td{
634
+ background:var(--dsw-alias-markdown-inline-code) !important;
635
+ color:inherit !important;
636
+ font-size:inherit !important;
637
+ padding:7px 10px !important;
638
+ vertical-align:top !important;
639
+ border:none !important;
640
+ border-radius:6px !important;
641
+ }
642
+ div[data-slot="conversation.chat.node"] table code,
643
+ div[data-slot="conversation.chat.node"] table pre{
644
+ background:transparent !important;
645
+ border:none !important;
646
+ box-shadow:none !important;
647
+ }
648
+ `
649
+
650
+ function buildRuntimeCss(value: ResolvedTweaks): string {
651
+ const rules: string[] = []
652
+ rules.push(buildFontCss(value.fontSize, value.lineHeight, value.codeFontSize))
653
+ // User-sent messages use their own fixed font-size (not the markdown tokens);
654
+ // route them through the same base so they follow the fontSize setting too.
655
+ // `steering` messages are sent while the agent is busy (busyEnter: steer);
656
+ // `[data-pending-steering]` is their in-flight bubble before it becomes durable.
657
+ rules.push(`[data-chat-flow-kind="user"] [class^="_text_"],[data-chat-flow-kind="steering"] [class^="_text_"],[data-pending-steering] [class^="_text_"]{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
658
+ // Composer input follows the same base size. The visible text is rendered by
659
+ // the hidden-textarea + backdrop/mirror pattern: the textarea is transparent,
660
+ // the `data-input-backdrop` paints the text you see, and `data-input-mirror`
661
+ // drives auto-grow. All three (plus the placeholder) must share the size.
662
+ // Only font-size is touched: line-height is part of the auto-grow/caret
663
+ // metrics, so overriding it misplaces the caret.
664
+ rules.push(`[data-composer-card="true"] textarea,[data-composer-card="true"] [data-input-backdrop],[data-composer-card="true"] [data-input-mirror]{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
665
+ rules.push(`[data-composer-card="true"] textarea::placeholder{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
666
+ // Markdown table cells are pinned by DSH (15px); let every table style follow
667
+ // the fontSize setting as well.
668
+ rules.push(`div[data-slot="conversation.chat.node"] table th,div[data-slot="conversation.chat.node"] table td{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
669
+ // Inline code is pinned by DSH to 0.875em of the surrounding text (it ignores
670
+ // the code token); scale that em by how far the chosen code size sits from
671
+ // the stock ratio (a 13px block at this body size).
672
+ const stockCodeBlock = value.fontSize * (13 / 16)
673
+ if (Math.abs(value.codeFontSize - stockCodeBlock) > 0.5) {
674
+ const em = (0.875 * (value.codeFontSize / stockCodeBlock)).toFixed(3)
675
+ rules.push(`div[data-slot="conversation.chat.node"] div[class*="_markdown_"] :not(pre)>code{font-size:${em}em !important}`)
676
+ }
677
+ // 行高: the base vertical rhythm of the reply area. When it differs from
678
+ // the stock 16px, every vertical spacing scales from it proportionally:
679
+ // ① gaps between message rows (Think ↔ tool cards like pwsh, user ↔
680
+ // assistant) — the chat flow column;
681
+ // ② gaps between content blocks inside one assistant reply (Think ↔ text) —
682
+ // the Assistant Markdown block container (`Sxvs8a_body`, CSS-module
683
+ // hashed, stable within the pinned rc.8 conversation package);
684
+ // ③ the markdown typography rhythm: paragraphs (`p` has a stock `margin:
685
+ // 16px 0` that must be overridden on both axes, not just bottom, or it
686
+ // can never tighten below 16), list margins, list-item gaps (stock 6px),
687
+ // paragraphs inside list items (stock 8px), code blocks, headings and
688
+ // rules — all scaled from the 行高 unit.
689
+ // Text line-height itself scales in buildFontCss above.
690
+ if (value.lineHeight !== DEFAULT_LINE_HEIGHT) {
691
+ const n = value.lineHeight
692
+ const md = `div[data-slot="conversation.chat.node"] div[class*="_markdown_"]`
693
+ const liGap = Math.max(2, Math.round((6 * n) / DEFAULT_LINE_HEIGHT))
694
+ const liPGap = Math.max(2, Math.round((8 * n) / DEFAULT_LINE_HEIGHT))
695
+ rules.push(`div[data-slot="conversation.view"] .Md3f7G_column{gap:${n}px !important}`)
696
+ rules.push(`div[data-slot="conversation.chat.node"] .Sxvs8a_body{gap:${n}px !important}`)
697
+ rules.push(`${md} p{margin:${n}px 0 !important}`)
698
+ rules.push(`${md} ul,${md} ol{margin:${n}px 0 !important}`)
699
+ rules.push(`${md} pre{margin:${n}px 0 !important}`)
700
+ // Code blocks: the pre's inner vertical padding is stock 16px (shiki
701
+ // default); scale it so the block's own height follows 行高 too. Only
702
+ // top/bottom is touched, horizontal padding stays as the theme sets it.
703
+ const codePad = Math.max(4, Math.round((16 * n) / DEFAULT_LINE_HEIGHT))
704
+ rules.push(`${md} pre{padding-top:${codePad}px !important;padding-bottom:${codePad}px !important}`)
705
+ // Table cells: vertical cell padding is stock 10px. Scale it for the
706
+ // default table style; the Claude preset keeps its own compact 7px cell
707
+ // padding (that look is the point of the preset).
708
+ if (value.tableStyle !== 'claude') {
709
+ const cellPad = Math.max(2, Math.round((10 * n) / DEFAULT_LINE_HEIGHT))
710
+ rules.push(`${md} table th,${md} table td{padding-top:${cellPad}px !important;padding-bottom:${cellPad}px !important}`)
711
+ }
712
+ rules.push(`${md} li:not(:first-child){margin-top:${liGap}px !important}`)
713
+ rules.push(`${md} li>p{margin:${liPGap}px 0 !important}`)
714
+ rules.push(`${md} h1,${md} h2,${md} h3{margin:${2 * n}px 0 ${n}px !important}`)
715
+ rules.push(`${md} h4,${md} h5,${md} h6{margin:${n}px 0 !important}`)
716
+ rules.push(`${md} hr{margin:${2 * n}px 0 !important}`)
717
+ rules.push(`${md} blockquote{margin:${n}px 0 0 !important}`)
718
+ // Keep DSH's flush edges: the first/last block of a reply has zero outer
719
+ // margin (stock `>*:first-child` / `>*:last-child`), so a message starts
720
+ // and ends tight and the spacing to the next node stays the column gap.
721
+ rules.push(`${md} > :first-child{margin-top:0 !important}`)
722
+ rules.push(`${md} > :last-child{margin-bottom:0 !important}`)
723
+ }
724
+ if (value.tableStyle === 'claude') {
725
+ rules.push(CLAUDE_TABLE_CSS)
726
+ }
727
+ return rules.join('\n')
728
+ }
729
+
730
+ function runtimeStyleElement(): HTMLStyleElement {
731
+ const id = 'dsh-ui-tweaks-runtime'
732
+ let style = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
733
+ if (style === null) {
734
+ style = document.createElement('style')
735
+ style.dataset.plugin = 'dsh-ui-tweaks'
736
+ style.dataset.pluginCss = id
737
+ document.head.appendChild(style)
738
+ }
739
+ return style
740
+ }
741
+
742
+ const BASE_CSS = `
743
+ .dut-settings{display:grid;gap:8px;max-width:680px;padding:4px 2px 24px;color:var(--dsw-alias-label-primary)}
744
+ .dut-settings-header{display:flex;align-items:flex-start;gap:10px;padding:2px 2px 0}
745
+ .dut-logo{flex:none;display:grid;place-items:center;width:30px;height:30px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1);background:linear-gradient(135deg,color-mix(in srgb,var(--dsw-alias-state-business-primary) 16%,transparent),transparent);line-height:1}
746
+ .dut-settings-header h2{font-size:16px;letter-spacing:-.01em;margin:0 0 2px}
747
+ .dut-settings-header p{max-width:600px;margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45}
748
+ .dut-panel{display:grid;gap:0;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-shadow-lv1);overflow:hidden}
749
+ .dut-section-label{font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);padding:9px 16px 4px}
750
+ .dut-field{display:grid;gap:6px;padding:7px 16px 10px}
751
+ .dut-field+.dut-field{border-top:1px solid var(--dsw-alias-border-l1)}
752
+ /* Two-column toggle grid: hairline dividers come from the 1px gap painting the
753
+ panel's border color through; cells repaint the panel background above it. */
754
+ .dut-grid{grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1px;background:var(--dsw-alias-border-l1)}
755
+ .dut-grid>.dut-section-label{grid-column:1/-1;background:var(--dsw-alias-bg-layer-1)}
756
+ .dut-grid .dut-field{background:var(--dsw-alias-bg-layer-1)}
757
+ .dut-grid .dut-field+.dut-field{border-top:none}
758
+ /* Subordinate rows (task-alert sub-toggles under the master switch): dimmed
759
+ whole-row so the dependency reads at a glance; buttons disable separately. */
760
+ .dut-grid .dut-field.dut-sub-off{opacity:.5}
761
+ /* Task-alerts section packs its six sub-toggle cells TWO per row (the master
762
+ row and the section label span all columns); very narrow panels fall back
763
+ to a single column so nothing squeezes. */
764
+ .dut-grid.dut-grid-half{grid-template-columns:repeat(2,minmax(0,1fr))}
765
+ .dut-grid .dut-field.dut-span-all{grid-column:1/-1}
766
+ @media (max-width:640px){.dut-grid.dut-grid-half{grid-template-columns:minmax(0,1fr)}}
767
+ .dut-field-top{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
768
+ .dut-field-top>span{font-size:13.5px;font-weight:600}
769
+ .dut-label{display:inline-flex;align-items:center;gap:6px}
770
+ .dut-hint{flex:none;display:inline-grid;place-items:center;width:15px;height:15px;border-radius:50%;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:9.5px;font-weight:600;font-style:normal;line-height:1;cursor:help;user-select:none;transition:color .15s ease,border-color .15s ease}
771
+ .dut-hint:hover,.dut-hint:focus-visible{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
772
+ .dut-hint-pop{position:fixed;z-index:9999;width:max-content;max-width:300px;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:11.5px;line-height:1.5;box-shadow:0 4px 16px rgba(0,0,0,.14);pointer-events:none}
773
+ .dut-controls{display:flex;align-items:center;gap:8px}
774
+ .dut-stepper{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}
775
+ .dut-stepper button{width:28px;height:28px;border:none;background:transparent;color:inherit;font-size:15px;font-weight:500;line-height:1;cursor:pointer;display:grid;place-items:center;transition:background .15s ease}
776
+ .dut-stepper button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
777
+ .dut-stepper button:disabled{opacity:.35;cursor:default}
778
+ .dut-stepper input{box-sizing:border-box;width:60px;height:28px;border:none;border-left:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);background:transparent;color:inherit;font:inherit;font-size:13px;text-align:center;-moz-appearance:textfield}
779
+ .dut-stepper input::-webkit-outer-spin-button,.dut-stepper input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
780
+ .dut-stepper input:focus{outline:none}
781
+ .dut-seg{display:inline-flex;padding:3px;gap:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
782
+ .dut-seg button{border:none;border-radius:7px;padding:5px 12px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer;transition:background .15s ease,color .15s ease}
783
+ .dut-seg button:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
784
+ .dut-seg button.dut-seg-active{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600;box-shadow:none}
785
+ .dut-seg button.dut-seg-active:hover:not(:disabled){color:var(--dsw-alias-state-business-primary)}
786
+ .dut-seg button:disabled{opacity:.45;cursor:default}
787
+ /* The selected Settings-section tab — the stock shell paints a barely-there
788
+ grey; brand-tint it so the selection reads clearly (and in the README shot). */
789
+ [role="dialog"] nav button[aria-selected="true"],[role="dialog"] nav button[aria-current="true"]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600}
790
+ .dut-btn{display:inline-flex;align-items:center;height:26px;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:11.5px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
791
+ .dut-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
792
+ .dut-btn.dut-btn-active{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent);color:var(--dsw-alias-state-business-primary)}
793
+ .dut-btn.dut-btn-active:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 18%,transparent);color:var(--dsw-alias-state-business-primary)}
794
+ .dut-btn:disabled{opacity:.4;cursor:default}
795
+ .dut-status{justify-self:start;font-size:11.5px;padding:3px 10px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary);animation:dut-fadein .18s ease}
796
+ @keyframes dut-fadein{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}
797
+ .dut-loading{padding:16px;border-radius:12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-secondary)}
798
+ .dut-alert{padding:10px 12px;border-radius:10px;font-size:12px;line-height:1.5}
799
+ .dut-alert.warning{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent);color:var(--dsw-alias-state-warn-label)}
800
+ .dut-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
801
+ `
802
+
803
+ function installBaseStyles(): () => void {
804
+ const id = 'dsh-ui-tweaks-base'
805
+ const existing = document.querySelector<HTMLStyleElement>(`style[data-plugin-css="${id}"]`)
806
+ if (existing !== null) return () => {}
807
+ const style = document.createElement('style')
808
+ style.dataset.plugin = 'dsh-ui-tweaks'
809
+ style.dataset.pluginCss = id
810
+ style.textContent = BASE_CSS
811
+ document.head.appendChild(style)
812
+ return () => { style.remove() }
813
+ }
814
+
815
+ async function apiRequest<T>(init?: RequestInit): Promise<T> {
816
+ const response = await fetch(SETTINGS_ROUTE, { credentials: 'same-origin', ...init })
817
+ const body = await response.json() as ApiSuccess<T> | ApiFailure
818
+ if (!response.ok || !body.ok) {
819
+ const failure = body as ApiFailure
820
+ throw new Error(failure.error?.message ?? `UI Tweaks request failed with HTTP ${response.status}`)
821
+ }
822
+ return body.value
823
+ }
824
+
825
+ /** Client-side snapshot store fed by the same-origin Settings route. */
826
+ interface SettingsState {
827
+ status: 'loading' | 'ready' | 'error'
828
+ writable: boolean
829
+ value: TweaksValue | undefined
830
+ revision: number | undefined
831
+ error?: string
832
+ }
833
+
834
+ /** Small external store shared by the Settings route and the CSS engine. */
835
+ export class SettingsClient {
836
+ private state: SettingsState = { status: 'loading', writable: false, value: undefined, revision: undefined }
837
+ private listeners = new Set<() => void>()
838
+ private generation = 0
839
+
840
+ subscribe = (listener: () => void): (() => void) => {
841
+ this.listeners.add(listener)
842
+ return () => { this.listeners.delete(listener) }
843
+ }
844
+
845
+ getSnapshot = (): SettingsState => this.state
846
+
847
+ private publish(next: SettingsState): void {
848
+ this.state = next
849
+ for (const listener of this.listeners) listener()
850
+ }
851
+
852
+ async load(): Promise<void> {
853
+ const generation = ++this.generation
854
+ if (this.state.status === 'loading') this.publish({ ...this.state, status: 'loading' })
855
+ try {
856
+ const snapshot = await apiRequest<UITweaksSnapshot>()
857
+ if (generation !== this.generation) return
858
+ this.publish({
859
+ status: 'ready',
860
+ writable: snapshot.writable,
861
+ value: snapshot.value,
862
+ revision: snapshot.revision,
863
+ })
864
+ } catch (error) {
865
+ if (generation !== this.generation) return
866
+ this.publish({ ...this.state, status: 'error', error: error instanceof Error ? error.message : String(error) })
867
+ }
868
+ }
869
+
870
+ private async post(payload: unknown): Promise<void> {
871
+ const generation = ++this.generation
872
+ const snapshot = await apiRequest<UITweaksSnapshot>({
873
+ method: 'POST',
874
+ headers: { 'Content-Type': 'application/json' },
875
+ body: JSON.stringify(payload),
876
+ })
877
+ if (generation !== this.generation) return
878
+ this.publish({
879
+ status: 'ready',
880
+ writable: snapshot.writable,
881
+ value: snapshot.value,
882
+ revision: snapshot.revision,
883
+ })
884
+ }
885
+
886
+ async set(field: string, value: unknown): Promise<void> {
887
+ await this.post({ action: 'set', field, value, expectedRevision: this.state.revision ?? 0 })
888
+ }
889
+
890
+ async unset(field: string): Promise<void> {
891
+ await this.post({ action: 'unset', field, expectedRevision: this.state.revision ?? 0 })
892
+ }
893
+ }
894
+
895
+ /** Required client services: slots (settings.section), locale, sessions (git bar, whale, archive, notifier), the slash-command registry, and the scope-addressed conversation face. */
896
+ export const inject = ['slots', 'locale', 'sessions', 'commandUi', 'conversation']
897
+
898
+ /**
899
+ * Hover/focus hint: a small ⓘ next to the field label; the hint text renders
900
+ * in a fixed-position bubble portaled to <body> (so panel `overflow:hidden`
901
+ * can never clip it), measured in a layout effect to prefer the space above
902
+ * the anchor and flip below near the viewport top. No layout shift: hints
903
+ * never occupy flow height.
904
+ */
905
+ function Hint({ text }: { text: string }) {
906
+ const anchorRef = useRef<HTMLSpanElement>(null)
907
+ const popRef = useRef<HTMLDivElement>(null)
908
+ const [open, setOpen] = useState(false)
909
+ const [pos, setPos] = useState<{ top: number; left: number }>({ top: -9999, left: -9999 })
910
+ useLayoutEffect(() => {
911
+ if (!open) return
912
+ const anchor = anchorRef.current?.getBoundingClientRect()
913
+ const pop = popRef.current
914
+ if (anchor === undefined || pop === null) return
915
+ let left = Math.min(Math.max(8, anchor.left), window.innerWidth - pop.offsetWidth - 8)
916
+ let top = anchor.top - pop.offsetHeight - 8
917
+ if (top < 8) top = anchor.bottom + 8
918
+ setPos({ top, left })
919
+ }, [open])
920
+ return (
921
+ <>
922
+ <span
923
+ ref={anchorRef}
924
+ className="dut-hint"
925
+ role="note"
926
+ aria-label={text}
927
+ tabIndex={0}
928
+ onMouseEnter={() => { setOpen(true) }}
929
+ onMouseLeave={() => { setOpen(false) }}
930
+ onFocus={() => { setOpen(true) }}
931
+ onBlur={() => { setOpen(false) }}
932
+ >i</span>
933
+ {open && createPortal(
934
+ <div ref={popRef} className="dut-hint-pop" style={{ top: pos.top, left: pos.left }}>{text}</div>,
935
+ document.body,
936
+ )}
937
+ </>
938
+ )
939
+ }
940
+
941
+ type SettingsSectionProps = PropsRuntime<'settings.section'> & {
942
+ controller: SettingsClient
943
+ t: Translate
944
+ }
945
+
946
+ function SettingsSection({ controller, t }: SettingsSectionProps) {
947
+ const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot)
948
+ const resolved = resolveValue(state.value)
949
+ const writable = state.writable
950
+ const [draft, setDraft] = useState<string>(String(resolved.fontSize))
951
+ const [codeDraft, setCodeDraft] = useState<string>(String(resolved.codeFontSize))
952
+ const [lineHeightDraft, setLineHeightDraft] = useState<string>(String(resolved.lineHeight))
953
+ const [status, setStatus] = useState<LocaleKey | undefined>(undefined)
954
+
955
+ useEffect(() => { if (state.status === 'loading' && state.value === undefined) void controller.load() }, [controller, state.status, state.value])
956
+ useEffect(() => { setDraft(String(resolved.fontSize)) }, [resolved.fontSize])
957
+ useEffect(() => { setCodeDraft(String(resolved.codeFontSize)) }, [resolved.codeFontSize])
958
+ useEffect(() => { setLineHeightDraft(String(resolved.lineHeight)) }, [resolved.lineHeight])
959
+ useEffect(() => {
960
+ if (status === undefined) return
961
+ const timer = setTimeout(() => { setStatus(undefined) }, 1800)
962
+ return () => { clearTimeout(timer) }
963
+ }, [status])
964
+
965
+ const commitFontSize = (raw: string): void => {
966
+ setDraft(raw)
967
+ const parsed = Number(raw)
968
+ if (!Number.isFinite(parsed)) return
969
+ const clamped = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(parsed)))
970
+ setDraft(String(clamped))
971
+ void controller.set('fontSize', clamped).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
972
+ }
973
+
974
+ const commitLineHeight = (raw: string): void => {
975
+ setLineHeightDraft(raw)
976
+ const parsed = Number(raw)
977
+ if (!Number.isFinite(parsed)) return
978
+ const clamped = Math.min(MAX_LINE_HEIGHT, Math.max(MIN_LINE_HEIGHT, Math.round(parsed)))
979
+ setLineHeightDraft(String(clamped))
980
+ void controller.set('lineHeight', clamped).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
981
+ }
982
+
983
+ const commitCodeSize = (raw: string): void => {
984
+ setCodeDraft(raw)
985
+ const parsed = Number(raw)
986
+ if (!Number.isFinite(parsed)) return
987
+ const clamped = Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, Math.round(parsed)))
988
+ setCodeDraft(String(clamped))
989
+ void controller.set('codeFontSize', clamped).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
990
+ }
991
+
992
+ const pickTableStyle = (raw: string): void => {
993
+ void controller.set('tableStyle', raw === 'claude' ? 'claude' : 'default').then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
994
+ }
995
+
996
+ const stepFontSize = (delta: number): void => {
997
+ const next = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, resolved.fontSize + delta))
998
+ setDraft(String(next))
999
+ void controller.set('fontSize', next).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1000
+ }
1001
+
1002
+ const stepLineHeight = (delta: number): void => {
1003
+ const next = Math.min(MAX_LINE_HEIGHT, Math.max(MIN_LINE_HEIGHT, resolved.lineHeight + delta))
1004
+ setLineHeightDraft(String(next))
1005
+ void controller.set('lineHeight', next).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1006
+ }
1007
+
1008
+ const stepCodeSize = (delta: number): void => {
1009
+ const next = Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, resolved.codeFontSize + delta))
1010
+ setCodeDraft(String(next))
1011
+ void controller.set('codeFontSize', next).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1012
+ }
1013
+
1014
+ const setGitBar = (value: boolean): void => {
1015
+ void controller.set('gitBarEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1016
+ }
1017
+
1018
+ const setArchiveManager = (value: boolean): void => {
1019
+ void controller.set('archiveManagerEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1020
+ }
1021
+
1022
+ const setMcpManager = (value: boolean): void => {
1023
+ void controller.set('mcpManagerEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1024
+ }
1025
+
1026
+ const setInitCommand = (value: boolean): void => {
1027
+ void controller.set('initCommandEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1028
+ }
1029
+
1030
+ const setWhaleIndicator = (value: boolean): void => {
1031
+ void controller.set('whaleIndicatorEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1032
+ }
1033
+
1034
+ const setPreciseCacheHit = (value: boolean): void => {
1035
+ void controller.set('preciseCacheHitEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1036
+ }
1037
+
1038
+ /** Master switch; enabling also asks for notification permission inside this click gesture. */
1039
+ const setNotifications = (value: boolean): void => {
1040
+ if (value) requestNotifyPermission()
1041
+ void controller.set('notificationsEnabled', value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1042
+ }
1043
+
1044
+ /** Channel/event toggles share one setter shape; the system-notification one requests permission too. */
1045
+ const setNotifyField = (field: 'notifyOnlyWhenHidden' | 'notifyOnComplete' | 'notifyOnInteraction' | 'notifyTitleFlash' | 'notifySystemNotification' | 'notifySound', value: boolean): void => {
1046
+ if (field === 'notifySystemNotification' && value) requestNotifyPermission()
1047
+ void controller.set(field, value).then(() => { setStatus('applied') }).catch(() => { setStatus('unavailable') })
1048
+ }
1049
+
1050
+ const testNotifications = (): void => {
1051
+ requestNotifyPermission()
1052
+ previewAlerts(
1053
+ {
1054
+ titleFlash: resolved.notifyTitleFlash,
1055
+ systemNotification: resolved.notifySystemNotification,
1056
+ sound: resolved.notifySound,
1057
+ },
1058
+ {
1059
+ notifyTitleDone: t('notifyTitleDone'),
1060
+ notifyTitleAborted: t('notifyTitleAborted'),
1061
+ notifyTitleFailed: t('notifyTitleFailed'),
1062
+ notifyTitlePending: t('notifyTitlePending'),
1063
+ bodyComplete: t('bodyComplete'),
1064
+ bodyAborted: t('bodyAborted'),
1065
+ bodyFailed: t('bodyFailed'),
1066
+ bodyApproval: t('bodyApproval'),
1067
+ bodyPlan: t('bodyPlan'),
1068
+ bodyQuestion: t('bodyQuestion'),
1069
+ },
1070
+ )
1071
+ }
1072
+
1073
+ const reset = (field: 'fontSize' | 'lineHeight' | 'tableStyle' | 'gitBarEnabled' | 'archiveManagerEnabled' | 'mcpManagerEnabled'): void => {
1074
+ void controller.unset(field).then(() => { setStatus('resetDone') }).catch(() => { setStatus('unavailable') })
1075
+ }
1076
+
1077
+ /** Code size reset clears BOTH keys: the px input and the legacy percentage. */
1078
+ const resetCodeSize = (): void => {
1079
+ void (async () => {
1080
+ try {
1081
+ await controller.unset('codeFontSize')
1082
+ await controller.unset('codeFontScale')
1083
+ setStatus('resetDone')
1084
+ } catch {
1085
+ setStatus('unavailable')
1086
+ }
1087
+ })()
1088
+ }
1089
+
1090
+ if (state.status === 'loading' && state.value === undefined) {
1091
+ return <div className="dut-settings"><div className="dut-loading">{t('loading')}</div></div>
1092
+ }
1093
+ if (state.status === 'error') {
1094
+ return <div className="dut-settings"><div className="dut-alert error">{t('unavailable')}</div></div>
1095
+ }
1096
+
1097
+ return (
1098
+ <div className="dut-settings">
1099
+ <header className="dut-settings-header">
1100
+ <div className="dut-logo"><ProjectLogoIcon className="dut-settings-logo" /></div>
1101
+ <div>
1102
+ <h2>{t('settingsTitle')}</h2>
1103
+ <p>{t('settingsIntro')}</p>
1104
+ </div>
1105
+ </header>
1106
+ {!writable ? <div className="dut-alert warning">{t('readOnly')}</div> : null}
1107
+ {status === undefined ? null : <div className="dut-status">{t(status)}</div>}
1108
+
1109
+ <section className="dut-panel">
1110
+ <div className="dut-section-label">{t('sectionText')}</div>
1111
+ <div className="dut-field">
1112
+ <div className="dut-field-top">
1113
+ <span className="dut-label">{t('fontSize')}<Hint text={t('fontSizeHint')} /></span>
1114
+ <div className="dut-controls">
1115
+ <div className="dut-stepper">
1116
+ <button type="button" aria-label="−" disabled={!writable || resolved.fontSize <= MIN_FONT_SIZE} onClick={() => { stepFontSize(-1) }}>−</button>
1117
+ <input
1118
+ type="number"
1119
+ min={MIN_FONT_SIZE}
1120
+ max={MAX_FONT_SIZE}
1121
+ step={1}
1122
+ value={draft}
1123
+ disabled={!writable}
1124
+ onChange={(event) => { setDraft(event.target.value) }}
1125
+ onBlur={(event) => { commitFontSize(event.target.value) }}
1126
+ onKeyDown={(event) => { if (event.key === 'Enter') commitFontSize((event.target as HTMLInputElement).value) }}
1127
+ />
1128
+ <button type="button" aria-label="+" disabled={!writable || resolved.fontSize >= MAX_FONT_SIZE} onClick={() => { stepFontSize(1) }}>+</button>
1129
+ </div>
1130
+ <button type="button" className={'dut-btn' + (resolved.fontSize === DEFAULT_FONT_SIZE ? ' dut-btn-active' : '')} disabled={!writable} onClick={() => { reset('fontSize') }}>{t('defaultAction')}</button>
1131
+ </div>
1132
+ </div>
1133
+ </div>
1134
+ <div className="dut-field">
1135
+ <div className="dut-field-top">
1136
+ <span className="dut-label">{t('lineHeight')}<Hint text={t('lineHeightHint')} /></span>
1137
+ <div className="dut-controls">
1138
+ <div className="dut-stepper">
1139
+ <button type="button" aria-label="−" disabled={!writable || resolved.lineHeight <= MIN_LINE_HEIGHT} onClick={() => { stepLineHeight(-2) }}>−</button>
1140
+ <input
1141
+ type="number"
1142
+ min={MIN_LINE_HEIGHT}
1143
+ max={MAX_LINE_HEIGHT}
1144
+ step={2}
1145
+ value={lineHeightDraft}
1146
+ disabled={!writable}
1147
+ onChange={(event) => { setLineHeightDraft(event.target.value) }}
1148
+ onBlur={(event) => { commitLineHeight(event.target.value) }}
1149
+ onKeyDown={(event) => { if (event.key === 'Enter') commitLineHeight((event.target as HTMLInputElement).value) }}
1150
+ />
1151
+ <button type="button" aria-label="+" disabled={!writable || resolved.lineHeight >= MAX_LINE_HEIGHT} onClick={() => { stepLineHeight(2) }}>+</button>
1152
+ </div>
1153
+ <button type="button" className={'dut-btn' + (resolved.lineHeight === DEFAULT_LINE_HEIGHT ? ' dut-btn-active' : '')} disabled={!writable} onClick={() => { reset('lineHeight') }}>{t('defaultAction')}</button>
1154
+ </div>
1155
+ </div>
1156
+ </div>
1157
+ <div className="dut-field">
1158
+ <div className="dut-field-top">
1159
+ <span className="dut-label">{t('codeFontSize')}<Hint text={t('codeFontSizeHint')} /></span>
1160
+ <div className="dut-controls">
1161
+ <div className="dut-stepper">
1162
+ <button type="button" aria-label="−" disabled={!writable || resolved.codeFontSize <= MIN_CODE_FONT_SIZE} onClick={() => { stepCodeSize(-1) }}>−</button>
1163
+ <input
1164
+ type="number"
1165
+ min={MIN_CODE_FONT_SIZE}
1166
+ max={MAX_CODE_FONT_SIZE}
1167
+ step={1}
1168
+ value={codeDraft}
1169
+ disabled={!writable}
1170
+ onChange={(event) => { setCodeDraft(event.target.value) }}
1171
+ onBlur={(event) => { commitCodeSize(event.target.value) }}
1172
+ onKeyDown={(event) => { if (event.key === 'Enter') commitCodeSize((event.target as HTMLInputElement).value) }}
1173
+ />
1174
+ <button type="button" aria-label="+" disabled={!writable || resolved.codeFontSize >= MAX_CODE_FONT_SIZE} onClick={() => { stepCodeSize(1) }}>+</button>
1175
+ </div>
1176
+ <button type="button" className={'dut-btn' + (resolved.codeFontSize === DEFAULT_CODE_FONT_SIZE ? ' dut-btn-active' : '')} disabled={!writable} onClick={() => { resetCodeSize() }}>{t('defaultAction')}</button>
1177
+ </div>
1178
+ </div>
1179
+ </div>
1180
+ <div className="dut-field">
1181
+ <div className="dut-field-top">
1182
+ <span className="dut-label">{t('tableStyle')}<Hint text={t('tableStyleHint')} /></span>
1183
+ <div className="dut-controls">
1184
+ <div className="dut-seg">
1185
+ <button type="button" className={resolved.tableStyle === 'claude' ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { pickTableStyle('claude') }}>{t('tableStyleClaude')}</button>
1186
+ <button type="button" className={resolved.tableStyle === 'default' ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { pickTableStyle('default') }}>{t('tableStyleDefault')}</button>
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ </div>
1191
+ </section>
1192
+
1193
+ <section className="dut-panel">
1194
+ <div className="dut-section-label">{t('sectionLayout')}</div>
1195
+ <div className="dut-field">
1196
+ <div className="dut-field-top">
1197
+ <span className="dut-label">{t('preciseCacheHit')}<Hint text={t('preciseCacheHitHint')} /></span>
1198
+ <div className="dut-controls">
1199
+ <div className="dut-seg">
1200
+ <button type="button" className={resolved.preciseCacheHitEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setPreciseCacheHit(true) }}>{t('preciseCacheHitOn')}</button>
1201
+ <button type="button" className={!resolved.preciseCacheHitEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setPreciseCacheHit(false) }}>{t('preciseCacheHitOff')}</button>
1202
+ </div>
1203
+ </div>
1204
+ </div>
1205
+ </div>
1206
+ </section>
1207
+
1208
+ <section className="dut-panel dut-grid">
1209
+ <div className="dut-section-label">{t('sectionFeatures')}</div>
1210
+ <div className="dut-field">
1211
+ <div className="dut-field-top">
1212
+ <span className="dut-label">{t('gitBar')}<Hint text={t('gitBarHint')} /></span>
1213
+ <div className="dut-controls">
1214
+ <div className="dut-seg">
1215
+ <button type="button" className={resolved.gitBarEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setGitBar(true) }}>{t('gitBarOn')}</button>
1216
+ <button type="button" className={!resolved.gitBarEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setGitBar(false) }}>{t('gitBarOff')}</button>
1217
+ </div>
1218
+ </div>
1219
+ </div>
1220
+ </div>
1221
+ <div className="dut-field">
1222
+ <div className="dut-field-top">
1223
+ <span className="dut-label">{t('archiveManager')}<Hint text={t('archiveManagerHint')} /></span>
1224
+ <div className="dut-controls">
1225
+ <div className="dut-seg">
1226
+ <button type="button" className={resolved.archiveManagerEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setArchiveManager(true) }}>{t('archiveManagerOn')}</button>
1227
+ <button type="button" className={!resolved.archiveManagerEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setArchiveManager(false) }}>{t('archiveManagerOff')}</button>
1228
+ </div>
1229
+ </div>
1230
+ </div>
1231
+ </div>
1232
+ <div className="dut-field">
1233
+ <div className="dut-field-top">
1234
+ <span className="dut-label">{t('mcpManager')}<Hint text={t('mcpManagerHint')} /></span>
1235
+ <div className="dut-controls">
1236
+ <div className="dut-seg">
1237
+ <button type="button" className={resolved.mcpManagerEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setMcpManager(true) }}>{t('mcpManagerOn')}</button>
1238
+ <button type="button" className={!resolved.mcpManagerEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setMcpManager(false) }}>{t('mcpManagerOff')}</button>
1239
+ </div>
1240
+ </div>
1241
+ </div>
1242
+ </div>
1243
+ <div className="dut-field">
1244
+ <div className="dut-field-top">
1245
+ <span className="dut-label">{t('initCommand')}<Hint text={t('initCommandHint')} /></span>
1246
+ <div className="dut-controls">
1247
+ <div className="dut-seg">
1248
+ <button type="button" className={resolved.initCommandEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setInitCommand(true) }}>{t('initCommandOn')}</button>
1249
+ <button type="button" className={!resolved.initCommandEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setInitCommand(false) }}>{t('initCommandOff')}</button>
1250
+ </div>
1251
+ </div>
1252
+ </div>
1253
+ </div>
1254
+ <div className="dut-field">
1255
+ <div className="dut-field-top">
1256
+ <span className="dut-label">{t('whaleIndicator')}<Hint text={t('whaleIndicatorHint')} /></span>
1257
+ <div className="dut-controls">
1258
+ <div className="dut-seg">
1259
+ <button type="button" className={resolved.whaleIndicatorEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setWhaleIndicator(true) }}>{t('whaleIndicatorOn')}</button>
1260
+ <button type="button" className={!resolved.whaleIndicatorEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setWhaleIndicator(false) }}>{t('whaleIndicatorOff')}</button>
1261
+ </div>
1262
+ </div>
1263
+ </div>
1264
+ </div>
1265
+ </section>
1266
+ <section className="dut-panel dut-grid dut-grid-half">
1267
+ <div className="dut-section-label">{t('sectionNotifications')}</div>
1268
+ <div className="dut-field dut-span-all">
1269
+ <div className="dut-field-top">
1270
+ <span className="dut-label">{t('notifications')}<Hint text={t('notificationsHint')} /></span>
1271
+ <div className="dut-controls">
1272
+ <div className="dut-seg">
1273
+ <button type="button" className={resolved.notificationsEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setNotifications(true) }}>{t('notifyOn')}</button>
1274
+ <button type="button" className={!resolved.notificationsEnabled ? 'dut-seg-active' : ''} disabled={!writable} onClick={() => { setNotifications(false) }}>{t('notifyOff')}</button>
1275
+ </div>
1276
+ <button type="button" className="dut-btn" disabled={!resolved.notificationsEnabled} onClick={() => { testNotifications() }}>{t('notifyTest')}</button>
1277
+ </div>
1278
+ </div>
1279
+ </div>
1280
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1281
+ <div className="dut-field-top">
1282
+ <span className="dut-label">{t('notifyOnComplete')}<Hint text={t('notifyOnCompleteHint')} /></span>
1283
+ <div className="dut-controls">
1284
+ <div className="dut-seg">
1285
+ <button type="button" className={resolved.notifyOnComplete ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnComplete', true) }}>{t('notifyOn')}</button>
1286
+ <button type="button" className={!resolved.notifyOnComplete ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnComplete', false) }}>{t('notifyOff')}</button>
1287
+ </div>
1288
+ </div>
1289
+ </div>
1290
+ </div>
1291
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1292
+ <div className="dut-field-top">
1293
+ <span className="dut-label">{t('notifyOnInteraction')}<Hint text={t('notifyOnInteractionHint')} /></span>
1294
+ <div className="dut-controls">
1295
+ <div className="dut-seg">
1296
+ <button type="button" className={resolved.notifyOnInteraction ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnInteraction', true) }}>{t('notifyOn')}</button>
1297
+ <button type="button" className={!resolved.notifyOnInteraction ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnInteraction', false) }}>{t('notifyOff')}</button>
1298
+ </div>
1299
+ </div>
1300
+ </div>
1301
+ </div>
1302
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1303
+ <div className="dut-field-top">
1304
+ <span className="dut-label">{t('notifyOnlyWhenHidden')}<Hint text={t('notifyOnlyWhenHiddenHint')} /></span>
1305
+ <div className="dut-controls">
1306
+ <div className="dut-seg">
1307
+ <button type="button" className={resolved.notifyOnlyWhenHidden ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnlyWhenHidden', true) }}>{t('notifyOn')}</button>
1308
+ <button type="button" className={!resolved.notifyOnlyWhenHidden ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyOnlyWhenHidden', false) }}>{t('notifyOff')}</button>
1309
+ </div>
1310
+ </div>
1311
+ </div>
1312
+ </div>
1313
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1314
+ <div className="dut-field-top">
1315
+ <span className="dut-label">{t('notifyTitleFlash')}<Hint text={t('notifyTitleFlashHint')} /></span>
1316
+ <div className="dut-controls">
1317
+ <div className="dut-seg">
1318
+ <button type="button" className={resolved.notifyTitleFlash ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyTitleFlash', true) }}>{t('notifyOn')}</button>
1319
+ <button type="button" className={!resolved.notifyTitleFlash ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifyTitleFlash', false) }}>{t('notifyOff')}</button>
1320
+ </div>
1321
+ </div>
1322
+ </div>
1323
+ </div>
1324
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1325
+ <div className="dut-field-top">
1326
+ <span className="dut-label">{t('notifySystemNotification')}<Hint text={t('notifySystemNotificationHint')} /></span>
1327
+ <div className="dut-controls">
1328
+ <div className="dut-seg">
1329
+ <button type="button" className={resolved.notifySystemNotification ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifySystemNotification', true) }}>{t('notifyOn')}</button>
1330
+ <button type="button" className={!resolved.notifySystemNotification ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifySystemNotification', false) }}>{t('notifyOff')}</button>
1331
+ </div>
1332
+ </div>
1333
+ </div>
1334
+ </div>
1335
+ <div className={'dut-field' + (!resolved.notificationsEnabled ? ' dut-sub-off' : '')}>
1336
+ <div className="dut-field-top">
1337
+ <span className="dut-label">{t('notifySound')}<Hint text={t('notifySoundHint')} /></span>
1338
+ <div className="dut-controls">
1339
+ <div className="dut-seg">
1340
+ <button type="button" className={resolved.notifySound ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifySound', true) }}>{t('notifyOn')}</button>
1341
+ <button type="button" className={!resolved.notifySound ? 'dut-seg-active' : ''} disabled={!writable || !resolved.notificationsEnabled} onClick={() => { setNotifyField('notifySound', false) }}>{t('notifyOff')}</button>
1342
+ </div>
1343
+ </div>
1344
+ </div>
1345
+ </div>
1346
+ </section>
1347
+ </div>
1348
+ )
1349
+ }
1350
+
1351
+ /**
1352
+ * The /init bootstrap prompts, submitted verbatim into the current session on
1353
+ * pick. Modeled after the classic coding-agent `/init`: explore the project,
1354
+ * then write (or improve) a root AGENTS.md addressed to future AI agents.
1355
+ */
1356
+ const INIT_PROMPT_ZH = [
1357
+ '你是 ChatCode CLI 编码代理。请为本项目生成一份面向未来编码代理的 AGENTS.md,放在仓库根目录:',
1358
+ '',
1359
+ '1. 先自行探索项目——阅读 README、清单文件(package.json / pyproject.toml 等)、构建脚本与关键源码目录,不要向我追问这些信息;',
1360
+ '2. AGENTS.md 用中文撰写,包含:项目简介、常用命令(安装 / 构建 / 测试 / 检查)、代码风格与约定、目录结构导览、已知注意事项;',
1361
+ '3. 只写从代码中验证过的事实,保持简洁(建议不超过 150 行),不要臆测;',
1362
+ '4. 如果已存在 AGENTS.md,在其基础上改进补充,不要丢失已有内容;',
1363
+ '5. 项目共享规则写入 AGENTS.md;如果发现只应在本机生效的私有说明,只建议用户放入 CHATCODE-CLI.local.md,不要自动创建或覆盖该文件;',
1364
+ '6. 完成后简要汇报你写了什么。',
1365
+ ].join('\n')
1366
+
1367
+ const INIT_PROMPT_EN = [
1368
+ 'You are the ChatCode CLI coding agent. Generate an AGENTS.md for this project at the repository root, addressed to future coding agents:',
1369
+ '',
1370
+ '1. Explore the project first — read the README, manifest files (package.json / pyproject.toml etc.), build scripts and key source directories; do not ask me about them.',
1371
+ '2. Write the AGENTS.md in English covering: project overview, common commands (install / build / test / lint), code style and conventions, a directory guide, and known gotchas.',
1372
+ '3. Only state facts verified from the code; keep it concise (~150 lines max); no speculation.',
1373
+ '4. If an AGENTS.md already exists, improve it in place without losing existing content.',
1374
+ '5. Put shared project instructions in AGENTS.md. If you discover machine-private guidance, only recommend CHATCODE-CLI.local.md; do not create or overwrite it automatically.',
1375
+ '6. Briefly report what you wrote when done.',
1376
+ ].join('\n')
1377
+
1378
+ /**
1379
+ * Register the `/init` slash command: a client-owned contribution that pops a
1380
+ * language picker and submits the matching AGENTS.md bootstrap prompt into
1381
+ * the picked session via the scope-addressed conversation face
1382
+ * (`ctx.sessions.scope(id).conversation.send` — the same hop DSH's own
1383
+ * packages use). Contribution rows merge into the host catalog by name; the
1384
+ * description string is captured at registration time, so a mid-session
1385
+ * language switch refreshes it only after reload.
1386
+ */
1387
+ function registerInitCommand(ctx: UITweaksClientContext): () => void {
1388
+ const t = ctx.locale.bind(NS)
1389
+ return ctx.commandUi.register({
1390
+ name: 'init',
1391
+ description: t('initDesc'),
1392
+ available: () => true,
1393
+ ui: {
1394
+ kind: 'popupSelect',
1395
+ options: (): Promise<readonly SelectOption[]> => Promise.resolve([
1396
+ { id: 'zh', label: t('initOptionZh'), detail: t('initOptionZhDetail') },
1397
+ { id: 'en', label: t('initOptionEn'), detail: t('initOptionEnDetail') },
1398
+ ]),
1399
+ onSelect: async (option, session) => {
1400
+ const prompt = option.id === 'en' ? INIT_PROMPT_EN : INIT_PROMPT_ZH
1401
+ const scoped = ctx.sessions.scope(session.sessionId)
1402
+ if (scoped === undefined) {
1403
+ console.error(`[ChatCode CLI UI Tweaks] ${t('initFailed')}: session ${session.sessionId} is not scoped`)
1404
+ return
1405
+ }
1406
+ // Route through the session's input facade (draft + submit) instead of
1407
+ // a bare conversation.send: the hub's own send choreography then
1408
+ // handles first-message materialization for a brand-new session, plus
1409
+ // queue/steer policy while a turn is running. A direct send cannot
1410
+ // start an unmaterialized session and fails silently there.
1411
+ const input = ctx.conversation.input.for(scoped)
1412
+ input.setDraft(prompt)
1413
+ input.submit()
1414
+ },
1415
+ },
1416
+ })
1417
+ }
1418
+
1419
+ /**
1420
+ * Register a Settings section only while its enable flag is on, so the
1421
+ * Settings nav row appears / disappears live with the toggle in the UI Tweaks
1422
+ * section. Waits for the `settings.section` declaration (like the static
1423
+ * inject), then keeps the section registered exactly while
1424
+ * `isEnabled(controller value)` holds: toggling the switch in the UI Tweaks
1425
+ * section mounts or disposes the section, and the settings shell's nav
1426
+ * (projected from the section ledger) updates in place.
1427
+ */
1428
+ function installConditionalSection(
1429
+ ctx: ClientContext,
1430
+ controller: SettingsClient,
1431
+ isEnabled: (value: TweaksValue | undefined) => boolean,
1432
+ registerSection: () => () => void,
1433
+ ): void {
1434
+ ctx.slots.inject('settings.section', () => {
1435
+ let dispose: (() => void) | undefined
1436
+ const sync = (): void => {
1437
+ const enabled = isEnabled(controller.getSnapshot().value)
1438
+ if (enabled && dispose === undefined) {
1439
+ dispose = registerSection()
1440
+ } else if (!enabled && dispose !== undefined) {
1441
+ dispose()
1442
+ dispose = undefined
1443
+ }
1444
+ }
1445
+ sync()
1446
+ const unsubscribe = controller.subscribe(sync)
1447
+ return () => {
1448
+ unsubscribe()
1449
+ dispose?.()
1450
+ }
1451
+ })
1452
+ }
1453
+
1454
+ export function apply(ctx: UITweaksClientContext): void {
1455
+ ctx.effect(installBranding, 'dsh-ui-tweaks: ChatCode CLI branding')
1456
+ ctx.effect(installBaseStyles, 'dsh-ui-tweaks: base styles')
1457
+ ctx.effect(installGitBarStyles, 'dsh-ui-tweaks: gitbar styles')
1458
+ ctx.effect(installArchiveStyles, 'dsh-ui-tweaks: archive styles')
1459
+ ctx.effect(installMcpStyles, 'dsh-ui-tweaks: mcp styles')
1460
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-ui-tweaks: locale')
1461
+ const t = ctx.locale.bind(NS)
1462
+
1463
+ const controller = new SettingsClient()
1464
+
1465
+ ctx.effect(() => {
1466
+ const applyCss = (): void => {
1467
+ const state = controller.getSnapshot()
1468
+ if (state.status === 'ready') {
1469
+ runtimeStyleElement().textContent = buildRuntimeCss(resolveValue(state.value))
1470
+ }
1471
+ }
1472
+ applyCss()
1473
+ const dispose = controller.subscribe(applyCss)
1474
+ void controller.load()
1475
+ return dispose
1476
+ }, 'dsh-ui-tweaks: runtime css')
1477
+
1478
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1479
+ name: 'settings.section',
1480
+ id: NS,
1481
+ order: 40,
1482
+ label: () => t('nav'),
1483
+ inject: () => ({ controller, t }),
1484
+ }, SettingsSection))
1485
+
1486
+ // The new-session hero exposes a dedicated brand-mark slot, so use the same
1487
+ // packaged logo there rather than restyling the host's fallback FishLogo.
1488
+ ctx.slots.inject('conversation.hero.brand.mark', () => ctx.slots.register({
1489
+ name: 'conversation.hero.brand.mark',
1490
+ priority: 10,
1491
+ }, HeroProjectLogo))
1492
+
1493
+ // The sidebar renders this same slot in its expanded header and collapsed
1494
+ // rail. Registering the packaged China Unicom knot here prevents the rail
1495
+ // from falling back to the host FishLogo after the sidebar is collapsed.
1496
+ ctx.slots.inject('sidebar.brand.mark', () => ctx.slots.register({
1497
+ name: 'sidebar.brand.mark',
1498
+ priority: 10,
1499
+ }, SidebarProjectLogo))
1500
+
1501
+ // Newer DSH hosts expose the expanded wordmark as its own slot. Owning it
1502
+ // directly avoids inheriting the host's brand typography; older hosts that
1503
+ // do not render this slot still use installBranding's DOM compatibility
1504
+ // path above.
1505
+ ctx.slots.inject('sidebar.brand.name', () => ctx.slots.register({
1506
+ name: 'sidebar.brand.name',
1507
+ priority: 10,
1508
+ }, SidebarProjectName))
1509
+
1510
+ // GitBar v3: the branch chip lives in the session header's action row
1511
+ // (beside the title, AFTER the mode badge) and the 打开项目/终端/差异 icon
1512
+ // group sits in the header's right-aligned utilities. Both are strict
1513
+ // session slots; each renders null outside git repos.
1514
+ ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
1515
+ name: 'conversation.session.header.actions',
1516
+ id: 'gitbar-branch',
1517
+ order: 10,
1518
+ locale: NS,
1519
+ inject: () => ({ controller, sessionsService: ctx.sessions }),
1520
+ }, BranchChipEntry))
1521
+
1522
+ // Warmup: the header chips mount only after the conversation projection
1523
+ // finishes loading (seconds on large inactive sessions). The input dock
1524
+ // mounts immediately on session switch, so a null-rendering seat there
1525
+ // prefetches the status into the shared cache right away — the chips then
1526
+ // paint folder+branch from the warm cache the moment they appear.
1527
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1528
+ name: 'conversation.input.dock',
1529
+ id: 'gitbar-warmup',
1530
+ order: 41,
1531
+ inject: () => ({}),
1532
+ }, GitWarmup))
1533
+
1534
+ ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({
1535
+ name: 'conversation.session.header.utilities',
1536
+ id: 'gitbar-utils',
1537
+ order: 10,
1538
+ locale: NS,
1539
+ inject: () => ({ controller }),
1540
+ }, HeaderUtilities))
1541
+
1542
+ // Hero (new-session screen): the session header does not mount there, so a
1543
+ // floating branch chip anchors beside the workspace picker instead — only
1544
+ // when the picked workspace is a git repo.
1545
+ ctx.effect(() => installHeroChip(controller, t), 'dsh-ui-tweaks: hero branch chip')
1546
+
1547
+ // Archive manager: a Settings section ("归档") that lists archived sessions
1548
+ // and can restore or permanently delete them. Registered only while the
1549
+ // archiveManagerEnabled toggle in the UI Tweaks section is on, so the nav
1550
+ // row appears / disappears live with the switch (the settings shell
1551
+ // projects its nav from the section ledger).
1552
+ installConditionalSection(ctx, controller, (value) => value?.archiveManagerEnabled === true, () => ctx.slots.register({
1553
+ name: 'settings.section',
1554
+ id: 'archive',
1555
+ order: 50,
1556
+ label: () => t('archiveNav'),
1557
+ locale: NS,
1558
+ inject: () => ({ controller, t, sessionsService: ctx.sessions }),
1559
+ }, ArchiveSection))
1560
+
1561
+ // MCP manager: a Settings section ("MCP 管理") that lists the configured MCP
1562
+ // servers with their status and tools, and restarts them. Registered only
1563
+ // while the mcpManagerEnabled toggle in the UI Tweaks section is on, so the
1564
+ // nav row appears / disappears live with the switch.
1565
+ installConditionalSection(ctx, controller, (value) => value?.mcpManagerEnabled === true, () => ctx.slots.register({
1566
+ name: 'settings.section',
1567
+ id: 'mcp',
1568
+ order: 60,
1569
+ label: () => t('mcpNav'),
1570
+ locale: NS,
1571
+ inject: () => ({ controller, t }),
1572
+ }, McpSection))
1573
+
1574
+ // /init slash command: registered only while the initCommandEnabled toggle
1575
+ // in the UI Tweaks section is on, so `/init` appears in the slash menu only
1576
+ // while the feature is enabled — same live register/dispose choreography as
1577
+ // the conditional Settings sections above.
1578
+ ctx.effect(() => {
1579
+ let dispose: (() => void) | undefined
1580
+ const sync = (): void => {
1581
+ const enabled = controller.getSnapshot().value?.initCommandEnabled === true
1582
+ if (enabled && dispose === undefined) {
1583
+ dispose = registerInitCommand(ctx)
1584
+ } else if (!enabled && dispose !== undefined) {
1585
+ dispose()
1586
+ dispose = undefined
1587
+ }
1588
+ }
1589
+ sync()
1590
+ return controller.subscribe(sync)
1591
+ }, 'dsh-ui-tweaks: /init command')
1592
+
1593
+ // Whale working indicator: the brand whale centered above the composer
1594
+ // card — translucent and still while idle, swimming while the model works.
1595
+ // The dock entry is registered only while the whaleIndicatorEnabled toggle
1596
+ // is on, so off costs nothing; styles ride the same toggle so an unused
1597
+ // keyframes rule never lingers in <head>.
1598
+ ctx.effect(() => {
1599
+ let disposeEntry: (() => void) | undefined
1600
+ let disposeStyles: (() => void) | undefined
1601
+ const sync = (): void => {
1602
+ const enabled = controller.getSnapshot().value?.whaleIndicatorEnabled === true
1603
+ if (enabled && disposeEntry === undefined) {
1604
+ disposeStyles = installWhaleStyles()
1605
+ disposeEntry = ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
1606
+ name: 'conversation.input.dock',
1607
+ id: 'whale',
1608
+ order: 30,
1609
+ inject: () => ({ sessionsService: ctx.sessions }),
1610
+ }, WhaleIndicator))
1611
+ } else if (!enabled && disposeEntry !== undefined) {
1612
+ disposeEntry()
1613
+ disposeEntry = undefined
1614
+ disposeStyles?.()
1615
+ disposeStyles = undefined
1616
+ }
1617
+ }
1618
+ sync()
1619
+ return controller.subscribe(sync)
1620
+ }, 'dsh-ui-tweaks: whale indicator')
1621
+
1622
+ // Precise cache hit: rewrites the stats line's cache-hit figure to two
1623
+ // decimals from the raw tokenUsage projection. The composer-dock entry
1624
+ // renders nothing itself — it is a live reader that patches the stock span
1625
+ // in place — and it is registered only while the preciseCacheHitEnabled
1626
+ // toggle is on, so off costs nothing and toggling restores the stock text.
1627
+ ctx.effect(() => {
1628
+ let disposeEntry: (() => void) | undefined
1629
+ const sync = (): void => {
1630
+ const enabled = controller.getSnapshot().value?.preciseCacheHitEnabled === true
1631
+ if (enabled && disposeEntry === undefined) {
1632
+ disposeEntry = ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
1633
+ name: 'conversation.composer.dock',
1634
+ id: 'precise-cache-hit',
1635
+ order: 90,
1636
+ inject: () => ({}),
1637
+ }, PreciseCacheHitEntry))
1638
+ } else if (!enabled && disposeEntry !== undefined) {
1639
+ disposeEntry()
1640
+ disposeEntry = undefined
1641
+ }
1642
+ }
1643
+ sync()
1644
+ return controller.subscribe(sync)
1645
+ }, 'dsh-ui-tweaks: precise cache hit')
1646
+
1647
+ // Task notifications: watch every session on the list feed and raise
1648
+ // tab-title / system-notification / chime alerts when one finishes its turn
1649
+ // or starts blocking on the user. Registered only while the master toggle
1650
+ // is on; sub-toggles are re-read live through readState, so flipping them
1651
+ // never reinstalls the watcher.
1652
+ ctx.effect(() => {
1653
+ let disposeNotifier: (() => void) | undefined
1654
+ const sync = (): void => {
1655
+ const enabled = controller.getSnapshot().value?.notificationsEnabled === true
1656
+ if (enabled && disposeNotifier === undefined) {
1657
+ disposeNotifier = installTaskNotifier({
1658
+ sessionsService: ctx.sessions,
1659
+ pendingInteractions: ctx.uiSession.pendingInteractions,
1660
+ text: {
1661
+ notifyTitleDone: t('notifyTitleDone'),
1662
+ notifyTitleAborted: t('notifyTitleAborted'),
1663
+ notifyTitleFailed: t('notifyTitleFailed'),
1664
+ notifyTitlePending: t('notifyTitlePending'),
1665
+ bodyComplete: t('bodyComplete'),
1666
+ bodyAborted: t('bodyAborted'),
1667
+ bodyFailed: t('bodyFailed'),
1668
+ bodyApproval: t('bodyApproval'),
1669
+ bodyPlan: t('bodyPlan'),
1670
+ bodyQuestion: t('bodyQuestion'),
1671
+ },
1672
+ readState: () => {
1673
+ const value = controller.getSnapshot().value
1674
+ return {
1675
+ options: {
1676
+ onlyWhenHidden: value?.notifyOnlyWhenHidden ?? true,
1677
+ onComplete: value?.notifyOnComplete ?? true,
1678
+ onInteraction: value?.notifyOnInteraction ?? true,
1679
+ },
1680
+ channels: {
1681
+ titleFlash: value?.notifyTitleFlash ?? true,
1682
+ systemNotification: value?.notifySystemNotification ?? true,
1683
+ sound: value?.notifySound ?? false,
1684
+ },
1685
+ }
1686
+ },
1687
+ })
1688
+ } else if (!enabled && disposeNotifier !== undefined) {
1689
+ disposeNotifier()
1690
+ disposeNotifier = undefined
1691
+ }
1692
+ }
1693
+ sync()
1694
+ return controller.subscribe(sync)
1695
+ }, 'dsh-ui-tweaks: task notifications')
1696
+ }