@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,301 @@
1
+ import { useEffect, useId, useMemo, useState } from 'react'
2
+ import {
3
+ IconChevronLeftOutline14,
4
+ IconChevronRightOutline14,
5
+ IconLoadingOutline16,
6
+ IconRefreshOutline14,
7
+ } from '@deepseek-ai/dsh-client-ui-primitives'
8
+ import clsx from 'clsx'
9
+ // Type-only: pulls the settings shell's SlotMap merges ('settings.section').
10
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
11
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
12
+ import { api, SidebarApiError, type ChatCodeTokenDay } from './api.ts'
13
+ import { isZh, t } from './locales.ts'
14
+ import css from './UsageHeatmapSection.module.css'
15
+
16
+ /** Full section props: the settings shell's runtime share (close, …). */
17
+ export type UsageHeatmapSectionProps = PropsRuntime<'settings.section'>
18
+
19
+ /** One rendered day cell. */
20
+ interface DayCell {
21
+ day: number
22
+ key: string
23
+ today: boolean
24
+ future: boolean
25
+ tokens: number
26
+ tokensIn: number
27
+ tokensOut: number
28
+ }
29
+
30
+ /** One month query's outcome. */
31
+ type LoadState =
32
+ | { kind: 'loading' }
33
+ | { kind: 'ready'; days: Map<string, ChatCodeTokenDay> }
34
+ | { kind: 'signin' }
35
+ | { kind: 'error'; message: string }
36
+
37
+ const startOfMonth = (date: Date): Date => new Date(date.getFullYear(), date.getMonth(), 1)
38
+
39
+ const dayKey = (year: number, month: number, day: number): string =>
40
+ `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
41
+
42
+ /** Coerce one platform number defensively (absent/negative/garbage → 0). */
43
+ const num = (value: unknown): number => {
44
+ const parsed = typeof value === 'number' ? value : Number(value)
45
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0
46
+ }
47
+
48
+ /** Compact token count (40477872 → “4048万” / “40M”). */
49
+ const compactTokens = (value: number): string =>
50
+ new Intl.NumberFormat(isZh() ? 'zh-CN' : 'en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(value)
51
+
52
+ /** Full token count with separators (tooltip and a11y text). */
53
+ const fullTokens = (value: number): string => new Intl.NumberFormat().format(Math.round(value))
54
+
55
+ /** “2026年9月” / “September 2026”. */
56
+ const monthLabelOf = (anchor: Date): string =>
57
+ new Intl.DateTimeFormat(isZh() ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'long' }).format(anchor)
58
+
59
+ /** “9月14日” / “Sep 14”. */
60
+ const shortDateOf = (anchor: Date, day: number): string =>
61
+ new Intl.DateTimeFormat(isZh() ? 'zh-CN' : 'en-US', { month: 'short', day: 'numeric' }).format(new Date(anchor.getFullYear(), anchor.getMonth(), day))
62
+
63
+ export function UsageHeatmapSection(_props: UsageHeatmapSectionProps) {
64
+ const [anchor, setAnchor] = useState(() => startOfMonth(new Date()))
65
+ const [load, setLoad] = useState<LoadState>({ kind: 'loading' })
66
+ const [reloadKey, setReloadKey] = useState(0)
67
+ const [hover, setHover] = useState<{ cell: DayCell; left: number; top: number; below: boolean } | null>(null)
68
+ const tooltipId = useId()
69
+
70
+ useEffect(() => {
71
+ const dismiss = (): void => { setHover(null) }
72
+ window.addEventListener('scroll', dismiss, true)
73
+ window.addEventListener('resize', dismiss)
74
+ return () => {
75
+ window.removeEventListener('scroll', dismiss, true)
76
+ window.removeEventListener('resize', dismiss)
77
+ }
78
+ }, [])
79
+
80
+ useEffect(() => {
81
+ const controller = new AbortController()
82
+ setLoad({ kind: 'loading' })
83
+ setHover(null)
84
+ const year = anchor.getFullYear()
85
+ const month = anchor.getMonth()
86
+ const lastDay = new Date(year, month + 1, 0).getDate()
87
+ api.chatcodeTokenTrend(dayKey(year, month, 1), dayKey(year, month, lastDay), controller.signal)
88
+ .then(rows => {
89
+ const days = new Map<string, ChatCodeTokenDay>()
90
+ for (const row of rows) days.set(String(row.statDate), row)
91
+ setLoad({ kind: 'ready', days })
92
+ })
93
+ .catch((error: unknown) => {
94
+ if (controller.signal.aborted) return
95
+ const code = error instanceof SidebarApiError ? error.code : ''
96
+ if (code === 'chatcode-unavailable' || code === 'chatcode-unauthorized') {
97
+ setLoad({ kind: 'signin' })
98
+ return
99
+ }
100
+ setLoad({ kind: 'error', message: error instanceof Error ? error.message : String(error) })
101
+ })
102
+ return () => { controller.abort() }
103
+ }, [anchor, reloadKey])
104
+
105
+ const cells = useMemo<DayCell[]>(() => {
106
+ const year = anchor.getFullYear()
107
+ const month = anchor.getMonth()
108
+ const daysInMonth = new Date(year, month + 1, 0).getDate()
109
+ const today = new Date()
110
+ const sameMonth = year === today.getFullYear() && month === today.getMonth()
111
+ const futureMonth = year > today.getFullYear() || (year === today.getFullYear() && month > today.getMonth())
112
+ const days = load.kind === 'ready' ? load.days : null
113
+ const list: DayCell[] = []
114
+ for (let day = 1; day <= daysInMonth; day++) {
115
+ const row = days?.get(dayKey(year, month, day))
116
+ list.push({
117
+ day,
118
+ key: dayKey(year, month, day),
119
+ today: sameMonth && day === today.getDate(),
120
+ future: futureMonth || (sameMonth && day > today.getDate()),
121
+ tokens: num(row?.totalTokens),
122
+ tokensIn: num(row?.tokensIn),
123
+ tokensOut: num(row?.tokensOut),
124
+ })
125
+ }
126
+ return list
127
+ }, [anchor, load])
128
+
129
+ const stats = useMemo(() => {
130
+ let total = 0
131
+ let peak = 0
132
+ let peakDay = 0
133
+ let active = 0
134
+ for (const cell of cells) {
135
+ if (cell.future) continue
136
+ total += cell.tokens
137
+ if (cell.tokens > peak) {
138
+ peak = cell.tokens
139
+ peakDay = cell.day
140
+ }
141
+ if (cell.tokens > 0) active++
142
+ }
143
+ return { total, peak, peakDay, active }
144
+ }, [cells])
145
+
146
+ const leadingBlanks = (new Date(anchor.getFullYear(), anchor.getMonth(), 1).getDay() + 6) % 7
147
+ const weekdays = [...t('usageWeekdays')]
148
+ const nextMonthDisabled = anchor.getTime() >= startOfMonth(new Date()).getTime()
149
+ const reload = (): void => { setReloadKey(key => key + 1) }
150
+ const goPrev = (): void => { setAnchor(current => new Date(current.getFullYear(), current.getMonth() - 1, 1)) }
151
+ const goNext = (): void => { setAnchor(current => new Date(current.getFullYear(), current.getMonth() + 1, 1)) }
152
+
153
+ const heatLevel = (cell: DayCell): number =>
154
+ cell.future || cell.tokens === 0 || stats.peak === 0 ? 0 : Math.ceil(cell.tokens / stats.peak * 4)
155
+
156
+ const showDetails = (cell: DayCell, target: HTMLElement): void => {
157
+ if (cell.future) return
158
+ const rect = target.getBoundingClientRect()
159
+ const below = rect.top < 160
160
+ setHover({ cell, left: rect.left + rect.width / 2, top: below ? rect.bottom : rect.top, below })
161
+ }
162
+
163
+ return (
164
+ <div className={css.section}>
165
+ <div className={css.group}>
166
+ <div className={css.headerRow}>
167
+ <div className={css.monthNav}>
168
+ <button type="button" className={css.monthButton} aria-label={t('usagePrevMonth')} onClick={goPrev}>
169
+ <IconChevronLeftOutline14 size={14} />
170
+ </button>
171
+ <span className={css.monthLabel} aria-live="polite">{monthLabelOf(anchor)}</span>
172
+ <button
173
+ type="button"
174
+ className={css.monthButton}
175
+ aria-label={t('usageNextMonth')}
176
+ onClick={goNext}
177
+ disabled={nextMonthDisabled}
178
+ >
179
+ <IconChevronRightOutline14 size={14} />
180
+ </button>
181
+ </div>
182
+ <button type="button" className={css.refreshButton} onClick={reload} disabled={load.kind === 'loading'}>
183
+ <IconRefreshOutline14 size={14} className={load.kind === 'loading' ? css.spin : undefined} />
184
+ <span>{t('usageRefresh')}</span>
185
+ </button>
186
+ </div>
187
+
188
+ {load.kind === 'ready' ? (
189
+ <>
190
+ <div className={css.overview}>
191
+ <div className={css.total}>
192
+ <span className={css.statLabel}>{t('usageStatTotal')}</span>
193
+ <div className={css.totalNumber} title={fullTokens(stats.total)}>
194
+ <span className={css.totalValue}>{compactTokens(stats.total)}</span>
195
+ <span className={css.totalUnit}>Tokens</span>
196
+ </div>
197
+ </div>
198
+ <div className={css.stats}>
199
+ <div className={css.stat}>
200
+ <span className={css.statLabel}>{t('usageStatPeak')}</span>
201
+ <span className={css.statValue} title={stats.peakDay > 0 ? shortDateOf(anchor, stats.peakDay) : undefined}>{compactTokens(stats.peak)}</span>
202
+ </div>
203
+ <div className={css.stat}>
204
+ <span className={css.statLabel}>{t('usageStatActive')}</span>
205
+ <span className={css.statValue}>{stats.active}</span>
206
+ </div>
207
+ <div className={css.stat}>
208
+ <span className={css.statLabel}>{t('usageStatAvg')}</span>
209
+ <span className={css.statValue}>{compactTokens(stats.active > 0 ? stats.total / stats.active : 0)}</span>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div className={css.gridBody}>
215
+ <div className={css.weekdays} aria-hidden="true">
216
+ {weekdays.map((weekday, index) => <span key={index}>{weekday}</span>)}
217
+ </div>
218
+ <div className={css.grid} role="group" aria-label={monthLabelOf(anchor)}>
219
+ {Array.from({ length: leadingBlanks }, (_, index) => <span key={`blank-${index}`} aria-hidden="true" />)}
220
+ {cells.map(cell => (
221
+ <button
222
+ type="button"
223
+ key={cell.key}
224
+ className={clsx(css.cell, cell.future && css.cellFuture, cell.today && css.cellToday, hover?.cell === cell && css.cellHover)}
225
+ data-level={heatLevel(cell)}
226
+ disabled={cell.future}
227
+ aria-current={cell.today ? 'date' : undefined}
228
+ aria-describedby={hover?.cell === cell ? tooltipId : undefined}
229
+ aria-label={cell.future
230
+ ? shortDateOf(anchor, cell.day)
231
+ : `${shortDateOf(anchor, cell.day)} · ${t('usageDayTokens', { tokens: fullTokens(cell.tokens) })} · ${t('usageTokensIn', { tokens: fullTokens(cell.tokensIn) })} · ${t('usageTokensOut', { tokens: fullTokens(cell.tokensOut) })}`}
232
+ onMouseEnter={event => { showDetails(cell, event.currentTarget) }}
233
+ onMouseLeave={() => { setHover(null) }}
234
+ onFocus={event => { showDetails(cell, event.currentTarget) }}
235
+ onBlur={() => { setHover(null) }}
236
+ onClick={event => { showDetails(cell, event.currentTarget) }}
237
+ onKeyDown={event => {
238
+ if (event.key === 'Escape' && hover !== null) {
239
+ event.stopPropagation()
240
+ setHover(null)
241
+ }
242
+ }}
243
+ >
244
+ <span className={css.cellDay}>{cell.day}</span>
245
+ <span className={css.cellTokens} aria-hidden="true">{!cell.future && cell.tokens > 0 ? compactTokens(cell.tokens) : '\u00a0'}</span>
246
+ </button>
247
+ ))}
248
+ </div>
249
+ </div>
250
+
251
+ <div className={css.footer}>
252
+ {cells.some(cell => cell.today) && <span className={css.todayLabel}><span className={css.todayDot} />{t('usageToday')}</span>}
253
+ <div className={css.legend} aria-hidden="true">
254
+ <span>{t('usageLess')}</span>
255
+ {[0, 1, 2, 3, 4].map(level => (
256
+ <span key={level} className={css.legendSwatch} data-level={level} />
257
+ ))}
258
+ <span>{t('usageMore')}</span>
259
+ </div>
260
+ </div>
261
+
262
+ {stats.total === 0 && <div className={css.empty}>{t('usageEmpty')}</div>}
263
+ </>
264
+ ) : load.kind === 'loading' ? (
265
+ <div className={css.state} role="status">
266
+ <IconLoadingOutline16 className={css.spin} size={16} />
267
+ <span>{t('usageLoading')}</span>
268
+ </div>
269
+ ) : load.kind === 'signin' ? (
270
+ <div className={css.state}>{t('usageLoginRequired')}</div>
271
+ ) : (
272
+ <div className={css.stateError}>
273
+ <span>{load.message === '' ? t('usageError') : `${t('usageError')}:${load.message}`}</span>
274
+ <button type="button" className={css.retry} onClick={reload}>
275
+ {t('usageRetry')}
276
+ </button>
277
+ </div>
278
+ )}
279
+ </div>
280
+
281
+ {hover !== null && !hover.cell.future && (
282
+ <div
283
+ id={tooltipId}
284
+ className={clsx(css.tooltip, hover.below && css.tooltipBelow)}
285
+ role="tooltip"
286
+ style={{
287
+ left: Math.min(Math.max(hover.left, 124), window.innerWidth - 124),
288
+ top: hover.top,
289
+ }}
290
+ >
291
+ <span className={css.tooltipDate}>{shortDateOf(anchor, hover.cell.day)}{hover.cell.today && <span className={css.tooltipToday}>{t('usageToday')}</span>}</span>
292
+ <span className={css.tooltipTokens}>{t('usageDayTokens', { tokens: fullTokens(hover.cell.tokens) })}</span>
293
+ <div className={css.tooltipSplit}>
294
+ <span>{t('usageTokensIn', { tokens: fullTokens(hover.cell.tokensIn) })}</span>
295
+ <span>{t('usageTokensOut', { tokens: fullTokens(hover.cell.tokensOut) })}</span>
296
+ </div>
297
+ </div>
298
+ )}
299
+ </div>
300
+ )
301
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The "add plugin" modals (Side card settings → the dashed cards at the
3
+ * end of the 侧边栏内容 / 文件预览 grids): declare that the sidebar's
4
+ * extension points — tab pages and file previewers — are open to plugins
5
+ * (registered through `ctx.betterSidebar`), point at the GitHub topic page
6
+ * for discovery, and show the repo's recommended plugin catalog of the
7
+ * matching kind (name / url / description / install script).
8
+ *
9
+ * Per entry there are two actions:
10
+ * - 「跳转」opens the plugin's repo in a REAL new browser tab (window.open
11
+ * — a button, so the sidebar link takeover cannot reroute it);
12
+ * - 「安装」only COPIES the install script to the clipboard (writeClipboard)
13
+ * with a transient "已复制" feedback on the button — the user pastes and
14
+ * runs it wherever they manage their DSH profile. No terminal is opened,
15
+ * nothing is closed, nothing can fail outward.
16
+ *
17
+ * The body is extracted as {@link PluginListBody} so tests render it
18
+ * directly — the Modal primitive runs hooks unconditionally, so an open
19
+ * Modal must never be renderToString'd (same rule as the settingsFor popup
20
+ * in SideCardSection); the modal itself mounts only while open.
21
+ */
22
+ import { useState, type ReactNode } from 'react'
23
+ import { Modal, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
24
+ import type { BetterSidebarService } from './service.ts'
25
+ import { PLUGIN_TOPIC_URL, type PluginEntry } from './plugins-shared.ts'
26
+ import { builtinTabPlugins } from './plugins-tabs.ts'
27
+ import { builtinViewerPlugins } from './plugins-viewers.ts'
28
+ import { t } from './locales.ts'
29
+ import css from './SideCardSection.module.css'
30
+
31
+ /** Which extension point the modal is adding a plugin for. */
32
+ export type PluginKind = 'tab' | 'viewer'
33
+
34
+ /** The catalog of one kind (kept in two repo files: plugins-tabs.ts /
35
+ * plugins-viewers.ts). */
36
+ function catalogOf(kind: PluginKind): readonly PluginEntry[] {
37
+ return kind === 'tab' ? builtinTabPlugins : builtinViewerPlugins
38
+ }
39
+
40
+ /** How long the "已复制" feedback stays on the copy button. */
41
+ const COPIED_FEEDBACK_MS = 1500
42
+
43
+ /** The modal body: the GitHub topic button + the recommended plugin list
44
+ * with per-entry jump/copy buttons (extracted for direct testing). */
45
+ export function PluginListBody(props: { service: BetterSidebarService; kind: PluginKind }) {
46
+ const { service, kind } = props
47
+ // Which entry's copy button currently shows the "已复制" feedback.
48
+ const [copiedId, setCopiedId] = useState<string | null>(null)
49
+ // Live catalog filter (name / id / description). A free-text search keeps
50
+ // the list usable once the catalogs grow beyond a handful of entries:
51
+ // every keystroke narrows the rendered rows, so a long catalog never
52
+ // becomes a wall of scrolling.
53
+ const [query, setQuery] = useState('')
54
+
55
+ const catalog = catalogOf(kind)
56
+ const needle = query.trim().toLowerCase()
57
+ const matches = (entry: PluginEntry): boolean => {
58
+ if (needle === '') return true
59
+ const description = typeof entry.description === 'function' ? entry.description() : entry.description
60
+ return entry.name.toLowerCase().includes(needle)
61
+ || entry.id.toLowerCase().includes(needle)
62
+ || description.toLowerCase().includes(needle)
63
+ }
64
+ const filtered = catalog.filter(matches)
65
+ // Group by the optional category (stable order: category order of first
66
+ // appearance, uncategorized entries last under the plain list).
67
+ const groups = new Map<string | undefined, PluginEntry[]>()
68
+ for (const entry of filtered) {
69
+ const key = entry.category === undefined ? undefined : (typeof entry.category === 'function' ? entry.category() : entry.category)
70
+ const list = groups.get(key)
71
+ if (list === undefined) groups.set(key, [entry])
72
+ else list.push(entry)
73
+ }
74
+
75
+ /** Copy the entry's install script to the clipboard and flash the button's
76
+ * "已复制" label for a moment. The feedback ONLY appears after a
77
+ * successful write — when the clipboard is unavailable or denied
78
+ * (writeClipboard resolves false) nothing is shown, so the user is never
79
+ * told to paste a command that was not placed on the clipboard. Never
80
+ * closes anything, never throws outward. */
81
+ const copy = async (entry: PluginEntry): Promise<void> => {
82
+ const written = await writeClipboard(entry.install)
83
+ if (!written) return
84
+ setCopiedId(entry.id)
85
+ window.setTimeout(() => {
86
+ setCopiedId(current => (current === entry.id ? null : current))
87
+ }, COPIED_FEEDBACK_MS)
88
+ }
89
+
90
+ /** Open the plugin's repo in a REAL new browser tab (window.open — a
91
+ * button, so the sidebar link takeover cannot reroute it). */
92
+ const jump = (entry: PluginEntry): void => {
93
+ window.open(entry.url, '_blank', 'noopener')
94
+ }
95
+
96
+ /** One catalog row (extracted so the group render stays flat). */
97
+ const renderEntry = (entry: PluginEntry): ReactNode => (
98
+ <div key={entry.id} className={css.pluginEntry}>
99
+ <div className={css.pluginEntryHead}>
100
+ {/* The name is a BUTTON on the same window.open path as the
101
+ jump button: as an anchor it would be caught by the
102
+ document-capture link takeover (which ignores
103
+ target=_blank) and land in the sidebar browser. */}
104
+ <button
105
+ type="button"
106
+ className={css.pluginName}
107
+ aria-label={`${t('openPlugin')}: ${entry.name}`}
108
+ onClick={() => { jump(entry) }}
109
+ >
110
+ {entry.name}
111
+ </button>
112
+ <span className={css.pluginEntryActions}>
113
+ <button
114
+ type="button"
115
+ className={css.pluginJumpBtn}
116
+ aria-label={`${t('openPlugin')}: ${entry.name}`}
117
+ onClick={() => { jump(entry) }}
118
+ >
119
+ {t('openPlugin')}
120
+ </button>
121
+ <button
122
+ type="button"
123
+ className={css.pluginCopyBtn}
124
+ aria-label={`${t('copyInstall')}: ${entry.name}`}
125
+ onClick={() => { copy(entry) }}
126
+ >
127
+ {copiedId === entry.id ? t('copied') : t('copy')}
128
+ </button>
129
+ </span>
130
+ </div>
131
+ <div className={css.pluginDesc}>
132
+ {typeof entry.description === 'function' ? entry.description() : entry.description}
133
+ </div>
134
+ <code className={css.pluginInstall}>{entry.install}</code>
135
+ </div>
136
+ )
137
+
138
+ return (
139
+ <div className={css.pluginList}>
140
+ <button
141
+ type="button"
142
+ className={css.pluginTopicBtn}
143
+ onClick={() => { window.open(PLUGIN_TOPIC_URL, '_blank', 'noopener') }}
144
+ >
145
+ {t('addPluginsBrowseMore')}
146
+ </button>
147
+ <input
148
+ type="search"
149
+ className={css.pluginSearch}
150
+ placeholder={t('addPluginsSearch')}
151
+ aria-label={t('addPluginsSearch')}
152
+ value={query}
153
+ onChange={event => { setQuery(event.currentTarget.value) }}
154
+ />
155
+ <div className={css.groupHeading}>
156
+ <span>{t('addPluginsRecommended')}</span>
157
+ <span className={css.count}>{filtered.length}</span>
158
+ </div>
159
+ {catalog.length === 0 ? (
160
+ <div className={css.pluginEmpty}>{t('addPluginsEmpty')}</div>
161
+ ) : filtered.length === 0 ? (
162
+ <div className={css.pluginEmpty}>{t('addPluginsNoMatch')}</div>
163
+ ) : (
164
+ <div className={css.pluginEntries}>
165
+ {[...groups.entries()].map(([category, entries]) => (
166
+ <div key={category ?? '\u0000'} className={css.pluginGroup}>
167
+ {category !== undefined && (
168
+ <div className={css.pluginGroupHeading}>{category}</div>
169
+ )}
170
+ {entries.map(renderEntry)}
171
+ </div>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+ )
177
+ }
178
+
179
+ /** The modal itself (mounted only while open — see the module comment). */
180
+ export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void; kind: PluginKind }) {
181
+ const { service, onClose, kind } = props
182
+ return (
183
+ <Modal
184
+ open
185
+ onClose={onClose}
186
+ title={kind === 'tab' ? t('addPluginsTabCard') : t('addPluginsViewerCard')}
187
+ description={kind === 'tab' ? t('addPluginsTabDesc') : t('addPluginsViewerDesc')}
188
+ closeLabel={t('close')}
189
+ className={css.pluginModal}
190
+ footer={(
191
+ <button type="button" className={css.done} onClick={onClose}>
192
+ {t('settingsDone')}
193
+ </button>
194
+ )}
195
+ >
196
+ <PluginListBody service={service} kind={kind} />
197
+ </Modal>
198
+ )
199
+ }