@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,118 @@
1
+ .group {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 10px;
5
+ padding: 20px;
6
+ border: 1px solid var(--dsw-alias-border-l2);
7
+ border-radius: 16px;
8
+ background: var(--dsw-alias-bg-layer-3);
9
+ }
10
+
11
+ .headingRow,
12
+ .modelTop {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ gap: 12px;
17
+ }
18
+
19
+ .heading {
20
+ font-size: 13px;
21
+ line-height: 20px;
22
+ font-weight: 600;
23
+ color: var(--dsw-alias-label-primary);
24
+ }
25
+
26
+ .description,
27
+ .status,
28
+ .modelId,
29
+ .modelDescription,
30
+ .meta {
31
+ margin: 0;
32
+ font-size: 12px;
33
+ line-height: 18px;
34
+ color: var(--dsw-alias-label-tertiary);
35
+ }
36
+
37
+ .refresh {
38
+ flex: none;
39
+ padding: 4px 9px;
40
+ border: 1px solid var(--dsw-alias-border-l2);
41
+ border-radius: 7px;
42
+ background: transparent;
43
+ color: var(--dsw-alias-label-secondary);
44
+ font: inherit;
45
+ font-size: 12px;
46
+ line-height: 18px;
47
+ cursor: pointer;
48
+ }
49
+
50
+ .refresh:hover:not(:disabled) {
51
+ border-color: var(--dsw-alias-label-dimmed);
52
+ background: var(--dsw-alias-interactive-bg-hover);
53
+ color: var(--dsw-alias-label-primary);
54
+ }
55
+
56
+ .refresh:disabled { cursor: wait; opacity: 0.65; }
57
+
58
+ .refresh:focus-visible,
59
+ .model:focus-visible {
60
+ outline: 2px solid var(--dsw-alias-border-l4);
61
+ outline-offset: 2px;
62
+ }
63
+
64
+ .models {
65
+ display: grid;
66
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
67
+ gap: 8px;
68
+ }
69
+
70
+ .model {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 4px;
74
+ min-width: 0;
75
+ padding: 12px;
76
+ border: 1px solid var(--dsw-alias-border-l2);
77
+ border-radius: 10px;
78
+ background: transparent;
79
+ color: inherit;
80
+ font: inherit;
81
+ text-align: left;
82
+ cursor: pointer;
83
+ }
84
+
85
+ .model:hover {
86
+ border-color: var(--dsw-alias-label-dimmed);
87
+ background: var(--dsw-alias-interactive-bg-hover);
88
+ }
89
+
90
+ .modelSelected {
91
+ border-color: color-mix(in srgb, var(--dsw-alias-button-primary-fill) 48%, transparent);
92
+ background: var(--dsw-alias-interactive-bg-active);
93
+ }
94
+
95
+ .modelName {
96
+ min-width: 0;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ white-space: nowrap;
100
+ font-size: 13px;
101
+ line-height: 20px;
102
+ font-weight: 600;
103
+ color: var(--dsw-alias-label-primary);
104
+ }
105
+
106
+ .selected {
107
+ flex: none;
108
+ padding: 1px 6px;
109
+ border-radius: 999px;
110
+ background: color-mix(in srgb, var(--dsw-alias-button-primary-fill) 12%, transparent);
111
+ color: var(--dsw-alias-brand-primary);
112
+ font-size: 11px;
113
+ line-height: 16px;
114
+ }
115
+
116
+ .modelId { font-family: var(--dsw-font-code, var(--ds-font-family-code, monospace)); }
117
+ .modelDescription { color: var(--dsw-alias-label-secondary); }
118
+ .meta { font-variant-numeric: tabular-nums; }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The CodingPlan portion of the existing Side card settings page. This is
3
+ * intentionally an embedded settings group rather than a new sidebar tab:
4
+ * it mirrors a model-configuration surface while leaving the DSH host's own
5
+ * model page untouched.
6
+ */
7
+ import { useCallback, useEffect, useState } from 'react'
8
+ import { api, type CodingPlanCatalogue } from './api.ts'
9
+ import { t } from './locales.ts'
10
+ import css from './CodingPlanModelsSettings.module.css'
11
+
12
+ /** The stable plugin-settings location for the selected logical model id. */
13
+ export const CODING_PLAN_SETTINGS_ID = 'codingplan'
14
+ export const CODING_PLAN_SELECTED_MODEL_KEY = 'selectedModelId'
15
+
16
+ export interface CodingPlanModelsSettingsProps {
17
+ selectedModelId?: string
18
+ onSelect(modelId: string): void
19
+ }
20
+
21
+ const EMPTY_CATALOGUE: CodingPlanCatalogue = { status: 'loading', models: [] }
22
+
23
+ function tokenSummary(model: CodingPlanCatalogue['models'][number]): string | undefined {
24
+ if (model.contextWindow !== undefined) return t('codingPlanContextWindow', { count: model.contextWindow })
25
+ if (model.maxToken !== undefined) return t('codingPlanMaxToken', { count: model.maxToken })
26
+ return undefined
27
+ }
28
+
29
+ /** Read-only CodingPlan catalogue with a persisted model selector. */
30
+ export function CodingPlanModelsSettings({ selectedModelId, onSelect }: CodingPlanModelsSettingsProps) {
31
+ const [catalogue, setCatalogue] = useState<CodingPlanCatalogue>(EMPTY_CATALOGUE)
32
+ const [refreshing, setRefreshing] = useState(false)
33
+
34
+ const load = useCallback(async (refresh: boolean) => {
35
+ if (refresh) setRefreshing(true)
36
+ try {
37
+ setCatalogue(await (refresh ? api.codingPlanRefresh() : api.codingPlanModels()))
38
+ } catch {
39
+ // The host intentionally gives only a generic unavailable state. Keep
40
+ // that rule if its route itself cannot be reached.
41
+ setCatalogue({ status: 'unavailable', models: [] })
42
+ } finally {
43
+ if (refresh) setRefreshing(false)
44
+ }
45
+ }, [])
46
+
47
+ useEffect(() => { void load(false) }, [load])
48
+
49
+ return (
50
+ <div className={css.group} data-codingplan-models>
51
+ <div className={css.headingRow}>
52
+ <span className={css.heading}>{t('codingPlanTitle')}</span>
53
+ <button
54
+ type="button"
55
+ className={css.refresh}
56
+ onClick={() => { void load(true) }}
57
+ disabled={refreshing}
58
+ >
59
+ {refreshing ? t('codingPlanRefreshing') : t('codingPlanRefresh')}
60
+ </button>
61
+ </div>
62
+ <p className={css.description}>{t('codingPlanDesc')}</p>
63
+
64
+ {catalogue.status === 'loading' && (
65
+ <div className={css.status} aria-live="polite">{t('codingPlanLoading')}</div>
66
+ )}
67
+ {catalogue.status === 'unavailable' && catalogue.models.length === 0 && (
68
+ <div className={css.status} role="status">{t('codingPlanUnavailable')}</div>
69
+ )}
70
+ {catalogue.models.length > 0 && (
71
+ <div className={css.models} role="radiogroup" aria-label={t('codingPlanTitle')}>
72
+ {catalogue.models.map(model => {
73
+ const selected = model.logicalModelId === selectedModelId
74
+ const details = [model.accessType, tokenSummary(model)].filter((value): value is string => value !== undefined)
75
+ return (
76
+ <button
77
+ key={model.logicalModelId}
78
+ type="button"
79
+ role="radio"
80
+ aria-checked={selected}
81
+ className={`${css.model}${selected ? ` ${css.modelSelected}` : ''}`}
82
+ onClick={() => { onSelect(model.logicalModelId) }}
83
+ >
84
+ <span className={css.modelTop}>
85
+ <span className={css.modelName}>{model.displayName}</span>
86
+ {selected && <span className={css.selected}>{t('codingPlanSelected')}</span>}
87
+ </span>
88
+ <span className={css.modelId}>{model.logicalModelId}</span>
89
+ {model.description !== undefined && <span className={css.modelDescription}>{model.description}</span>}
90
+ {details.length > 0 && <span className={css.meta}>{details.join(' · ')}</span>}
91
+ </button>
92
+ )
93
+ })}
94
+ </div>
95
+ )}
96
+ {catalogue.status === 'unavailable' && catalogue.models.length > 0 && (
97
+ <div className={css.status} role="status">{t('codingPlanStale')}</div>
98
+ )}
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The diff tab: one change opened from the git panel, like VSCode's diff
3
+ * editor. A worktree ref loads the file's unified diff (`git diff`, staged or
4
+ * not; untracked files — which git diff never covers — render as a full-file
5
+ * addition from their content), a commit ref loads the commit's full patch
6
+ * (`git.show`-style). The header carries a refresh button because the tab
7
+ * stays mounted while the git panel's staging/discard operations change the
8
+ * very content it shows.
9
+ */
10
+ import { useCallback, useEffect, useState } from 'react'
11
+ import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
12
+ import type { SessionScope } from './api.ts'
13
+ import { api } from './api.ts'
14
+ import type { SidebarDiffRef } from './state.ts'
15
+ import { DiffView } from './DiffView.tsx'
16
+ import { t } from './locales.ts'
17
+ import { resolveSidebarPath } from './produced-files.ts'
18
+ import css from './sidebar.module.css'
19
+
20
+ /** The loaded diff surface (untracked content rendered as a full addition). */
21
+ interface DiffData {
22
+ diff: string
23
+ untracked?: string
24
+ }
25
+
26
+ export function DiffTab(props: { sessionId: string; cwd: string | undefined; diff: SidebarDiffRef }) {
27
+ const { sessionId, cwd, diff } = props
28
+ const [loading, setLoading] = useState(true)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const [data, setData] = useState<DiffData | null>(null)
31
+ const [tick, setTick] = useState(0)
32
+
33
+ const refresh = useCallback((): void => { setTick(value => value + 1) }, [])
34
+
35
+ useEffect(() => {
36
+ let cancelled = false
37
+ const scope: SessionScope = { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) }
38
+ setLoading(true)
39
+ setError(null)
40
+ setData(null)
41
+ const load = async (): Promise<void> => {
42
+ try {
43
+ if (diff.kind === 'commit') {
44
+ const result = await api.gitCommitDiff(scope, diff.hashFull, diff.worktree)
45
+ if (!cancelled) setData({ diff: result.diff })
46
+ return
47
+ }
48
+ let result = await api.gitDiff(scope, diff.path, diff.staged, diff.worktree)
49
+ if (result.diff === '') {
50
+ // The requested side is empty — try the OTHER side once: the ref
51
+ // may predate the staged-flag fix, or the change moved sides (a
52
+ // file staged after its tab opened). Both sides empty means the
53
+ // file genuinely has no text changes.
54
+ const other = await api.gitDiff(scope, diff.path, !diff.staged, diff.worktree)
55
+ if (other.diff !== '') result = other
56
+ }
57
+ if (result.diff !== '') {
58
+ if (!cancelled) setData({ diff: result.diff })
59
+ return
60
+ }
61
+ // Empty diff: an untracked file (git diff never lists it) falls back
62
+ // to a full-file addition; anything else is a genuine no-text-change.
63
+ if (diff.untracked === true && !diff.staged) {
64
+ // A child-repo path is relative to diff.repoRoot, not the session
65
+ // cwd or the linked-worktree root; resolve against whichever the
66
+ // diff ref carries so the untracked fallback reads the right file.
67
+ const text = await api.fsRead(scope, resolveSidebarPath(diff.repoRoot ?? diff.worktree ?? cwd, diff.path))
68
+ if (!cancelled) {
69
+ setData(text.kind === 'text' ? { diff: '', untracked: text.content } : { diff: '' })
70
+ }
71
+ return
72
+ }
73
+ if (!cancelled) setData({ diff: '' })
74
+ } catch (reason) {
75
+ if (!cancelled) setError(reason instanceof Error ? reason.message : String(reason))
76
+ } finally {
77
+ if (!cancelled) setLoading(false)
78
+ }
79
+ }
80
+ void load()
81
+ return () => { cancelled = true }
82
+ }, [sessionId, cwd, diff, tick])
83
+
84
+ return (
85
+ <div className={css.gitDiffTab}>
86
+ <div className={css.gitDiffTabHeader}>
87
+ <span className={css.gitDiffTabTitle} title={diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}>
88
+ {diff.kind === 'worktree' ? diff.path : `${diff.hash} ${diff.subject}`}
89
+ </span>
90
+ <button
91
+ type="button"
92
+ className={css.iconButton}
93
+ aria-label={t('refresh')}
94
+ title={t('refresh')}
95
+ onClick={refresh}
96
+ >
97
+ <IconRefreshOutline16 size={14} />
98
+ </button>
99
+ </div>
100
+ {loading && <div className={css.gitPlaceholder}>{t('loading')}</div>}
101
+ {!loading && error !== null && <div className={css.gitError}>{t('diffLoadError')}: {error}</div>}
102
+ {!loading && error === null && data !== null && (
103
+ <>
104
+ {data.untracked !== undefined
105
+ ? <DiffView diff="" untrackedPath={diff.kind === 'worktree' ? diff.path : ''} untrackedContent={data.untracked} />
106
+ : <DiffView diff={data.diff} />}
107
+ {data.diff === '' && data.untracked === undefined && (
108
+ <div className={css.gitEmpty}>{t('diffEmpty')}</div>
109
+ )}
110
+ </>
111
+ )}
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * The real diff surface for the git panel: parses the host's unified diff
3
+ * text (`git diff` / `git show`) and renders it VSCode-style — per-file
4
+ * sections with hunks (`@@ -a,b +c,d @@` headers), old/new line-number
5
+ * gutters, and aligned context / deleted / added rows colored through the
6
+ * DSH tokens. Untracked files produce no `git diff` output, so the caller
7
+ * can pass the file content to render as a full-file addition instead.
8
+ *
9
+ * The parser is a pure function (`parseUnifiedDiff`) so the interesting
10
+ * cases are unit-tested without a DOM.
11
+ */
12
+ import { useEffect, useMemo, useState, type ReactNode } from 'react'
13
+ import clsx from 'clsx'
14
+ import { t } from './locales.ts'
15
+ import css from './sidebar.module.css'
16
+
17
+ /** One rendered diff line. */
18
+ export interface DiffLine {
19
+ kind: 'ctx' | 'del' | 'add' | 'meta'
20
+ /** The line content without its diff marker ('' for the no-newline marker). */
21
+ text: string
22
+ /** Old-side line number (null for pure additions / metadata). */
23
+ oldNum: number | null
24
+ /** New-side line number (null for pure deletions / metadata). */
25
+ newNum: number | null
26
+ }
27
+
28
+ /** One parsed hunk. */
29
+ export interface DiffHunk {
30
+ /** The old-side start line (`-a[,b]`). */
31
+ oldStart: number
32
+ /** The new-side start line (`+c[,d]`). */
33
+ newStart: number
34
+ /** The section text after the trailing `@@` (may be empty). */
35
+ header: string
36
+ lines: DiffLine[]
37
+ }
38
+
39
+ /** One parsed file section of a unified diff. */
40
+ export interface DiffFile {
41
+ /** The `---` path verbatim ('/dev/null' for a new file). */
42
+ oldPath: string
43
+ /** The `+++` path verbatim ('/dev/null' for a deleted file). */
44
+ newPath: string
45
+ /** The file changed with binary content: no hunks to draw. */
46
+ binary: boolean
47
+ hunks: DiffHunk[]
48
+ }
49
+
50
+ /** The parsed unified diff. */
51
+ export interface ParsedDiff {
52
+ files: DiffFile[]
53
+ }
54
+
55
+ /** Parse the hunk header `@@ -a[,b] +c[,d] @@ section` (section may contain '@@'). */
56
+ function parseHunkHeader(line: string): { oldStart: number; newStart: number; header: string } | null {
57
+ const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/.exec(line)
58
+ if (match === null) return null
59
+ return { oldStart: Number(match[1]), newStart: Number(match[3]), header: match[5] ?? '' }
60
+ }
61
+
62
+ /**
63
+ * Parse `git diff --no-color` output into file sections and hunks. Rows
64
+ * outside a file section (leading noise) and metadata rows between the
65
+ * `diff --git`/`---`/`+++` headers and the first hunk (index lines, mode
66
+ * changes, rename/similarity lines) are skipped; a section that never
67
+ * reaches a hunk (a mode/rename-only change) stays hunkless so the caller
68
+ * can still draw its path.
69
+ */
70
+ export function parseUnifiedDiff(text: string): ParsedDiff {
71
+ const files: DiffFile[] = []
72
+ let current: DiffFile | null = null
73
+ let inHunk = false
74
+ let hunk: DiffHunk | null = null
75
+ let oldNum = 0
76
+ let newNum = 0
77
+ const flushHunk = (): void => {
78
+ if (current !== null && hunk !== null) current.hunks.push(hunk)
79
+ hunk = null
80
+ inHunk = false
81
+ }
82
+ for (const raw of text.split('\n')) {
83
+ if (raw.startsWith('diff --git ')) {
84
+ flushHunk()
85
+ current = { oldPath: '', newPath: '', binary: false, hunks: [] }
86
+ files.push(current)
87
+ continue
88
+ }
89
+ if (current === null) continue
90
+ if (raw.startsWith('Binary files ') || raw === 'GIT binary patch') {
91
+ flushHunk()
92
+ current.binary = true
93
+ continue
94
+ }
95
+ if (raw.startsWith('--- ')) {
96
+ flushHunk()
97
+ current.oldPath = raw.slice(4)
98
+ continue
99
+ }
100
+ if (raw.startsWith('+++ ')) {
101
+ current.newPath = raw.slice(4)
102
+ continue
103
+ }
104
+ const header = parseHunkHeader(raw)
105
+ if (header !== null) {
106
+ flushHunk()
107
+ hunk = { oldStart: header.oldStart, newStart: header.newStart, header: header.header, lines: [] }
108
+ oldNum = header.oldStart
109
+ newNum = header.newStart
110
+ inHunk = true
111
+ continue
112
+ }
113
+ if (!inHunk || hunk === null) continue
114
+ const marker = raw[0]
115
+ if (marker === '\\') {
116
+ // ``: metadata attached to the previous row.
117
+ hunk.lines.push({ kind: 'meta', text: raw.slice(1), oldNum: null, newNum: null })
118
+ continue
119
+ }
120
+ if (marker === ' ') {
121
+ hunk.lines.push({ kind: 'ctx', text: raw.slice(1), oldNum, newNum })
122
+ oldNum += 1
123
+ newNum += 1
124
+ } else if (marker === '-') {
125
+ hunk.lines.push({ kind: 'del', text: raw.slice(1), oldNum, newNum: null })
126
+ oldNum += 1
127
+ } else if (marker === '+') {
128
+ hunk.lines.push({ kind: 'add', text: raw.slice(1), oldNum: null, newNum })
129
+ newNum += 1
130
+ } else {
131
+ // Not a diff line (a hunk can never contain one): stop the hunk.
132
+ flushHunk()
133
+ }
134
+ }
135
+ flushHunk()
136
+ return { files }
137
+ }
138
+
139
+ /** Build the untracked-file shape: one file, one hunk of pure additions. */
140
+ function untrackedFile(path: string, content: string): DiffFile {
141
+ const lines: DiffLine[] = []
142
+ const body = content.endsWith('\n') ? content.slice(0, -1) : content
143
+ if (body !== '') {
144
+ let num = 1
145
+ for (const line of body.split('\n')) {
146
+ lines.push({ kind: 'add', text: line, oldNum: null, newNum: num })
147
+ num += 1
148
+ }
149
+ }
150
+ return { oldPath: '/dev/null', newPath: `b/${path}`, binary: false, hunks: [{ oldStart: 0, newStart: 1, header: '', lines }] }
151
+ }
152
+
153
+ /** Strip the `a/` / `b/` prefix git puts on diff paths (not on /dev/null). */
154
+ function displayPath(path: string): string {
155
+ if (path === '/dev/null') return path
156
+ if (path.startsWith('a/') || path.startsWith('b/')) return path.slice(2)
157
+ return path
158
+ }
159
+
160
+ /** The file header badge: added / deleted / renamed / binary ('' for a plain edit). */
161
+ function fileTag(file: DiffFile): string | null {
162
+ if (file.binary) return t('diffBinary')
163
+ if (file.oldPath === '/dev/null') return t('diffAdded')
164
+ if (file.newPath === '/dev/null') return t('diffDeleted')
165
+ const oldPath = displayPath(file.oldPath)
166
+ const newPath = displayPath(file.newPath)
167
+ if (oldPath !== newPath) return t('diffRenamed')
168
+ return null
169
+ }
170
+
171
+ /** Cap the flattened rows like DiffBlock: head + tail, expand button between. */
172
+ const MAX_DIFF_ROWS = 500
173
+
174
+ const TEST_PATH = /(^|\/)(?:__tests__|tests?|specs?|fixtures?|mocks?|snapshots?)(?:\/|$)|\.(?:test|spec)\.[^/]+$/i
175
+ const DOC_PATH = /(^|\/)(?:docs?|documentation)(?:\/|$)|(^|\/)(?:readme|changelog|contributing|license|authors|notice)(?:\.[^/]*)?$/i
176
+ const GENERATED_PATH = /(^|\/)(?:dist|build|coverage|generated|vendor|node_modules)(?:\/|$)|(^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|composer\.lock|cargo\.lock|poetry\.lock)$/i
177
+ const SOURCE_PATH = /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts|py|pyw|rb|php|java|kt|kts|scala|go|rs|swift|c|h|cc|cpp|cxx|hpp|hh|hxx|cs|fs|fsx|vb|dart|lua|r|ex|exs|erl|hrl|clj|cljs|cljc|groovy|sh|bash|zsh|fish|ps1|sql|vue|svelte|astro|html|htm|css|scss|sass|less)$/i
178
+
179
+ /** Source files open by default; tests, docs, generated files and unknown types stay folded. */
180
+ function defaultExpandedFiles(files: DiffFile[]): Set<number> {
181
+ const expanded = new Set<number>()
182
+ files.forEach((file, index) => {
183
+ const path = displayPath(file.newPath === '/dev/null' ? file.oldPath : file.newPath)
184
+ if (!file.binary && file.hunks.length > 0
185
+ && !TEST_PATH.test(path) && !DOC_PATH.test(path) && !GENERATED_PATH.test(path)
186
+ && SOURCE_PATH.test(path)) {
187
+ expanded.add(index)
188
+ }
189
+ })
190
+ return expanded
191
+ }
192
+
193
+ export interface DiffViewProps {
194
+ /** Unified diff text (`git.diff` or `git.commit-diff` payloads). */
195
+ diff: string
196
+ /** Untracked-file content: when present, renders as a full-file addition instead of parsing. */
197
+ untrackedPath?: string
198
+ untrackedContent?: string
199
+ }
200
+
201
+ export function DiffView({ diff, untrackedPath, untrackedContent }: DiffViewProps) {
202
+ const parsed = useMemo<ParsedDiff>(() => {
203
+ if (untrackedPath !== undefined) {
204
+ return { files: [untrackedFile(untrackedPath, untrackedContent ?? '')] }
205
+ }
206
+ return parseUnifiedDiff(diff)
207
+ }, [diff, untrackedPath, untrackedContent])
208
+ const [expanded, setExpanded] = useState(false)
209
+ const [expandedFiles, setExpandedFiles] = useState<Set<number>>(() => defaultExpandedFiles(parsed.files))
210
+
211
+ useEffect(() => { setExpandedFiles(defaultExpandedFiles(parsed.files)) }, [parsed])
212
+
213
+ // Flatten into display rows so the cap can slice a single list.
214
+ const rows = useMemo(() => {
215
+ const out: Array<{ key: string; file: DiffFile; fileIndex: number; type: 'path' | 'hunk' | 'line'; hunk?: DiffHunk; line?: DiffLine }> = []
216
+ parsed.files.forEach((file, fileIndex) => {
217
+ out.push({ key: `f${fileIndex}`, file, fileIndex, type: 'path' })
218
+ if (file.binary || !expandedFiles.has(fileIndex)) return
219
+ file.hunks.forEach((hunk, hunkIndex) => {
220
+ out.push({ key: `f${fileIndex}h${hunkIndex}`, file, fileIndex, type: 'hunk', hunk })
221
+ hunk.lines.forEach((line, lineIndex) => {
222
+ out.push({ key: `f${fileIndex}h${hunkIndex}l${lineIndex}`, file, fileIndex, type: 'line', hunk, line })
223
+ })
224
+ })
225
+ })
226
+ return out
227
+ }, [parsed, expandedFiles])
228
+
229
+ const hidden = rows.length - MAX_DIFF_ROWS
230
+ const capped = hidden > 0 && !expanded
231
+ const headLines = Math.ceil(MAX_DIFF_ROWS / 2)
232
+ const tailLines = MAX_DIFF_ROWS - headLines
233
+ const head = capped ? rows.slice(0, headLines) : rows
234
+ const tail = capped ? rows.slice(rows.length - tailLines) : []
235
+
236
+ if (rows.length === 0) return null
237
+
238
+ const renderRow = (row: (typeof rows)[number]): ReactNode => {
239
+ if (row.type === 'path') {
240
+ const tag = fileTag(row.file)
241
+ const from = displayPath(row.file.oldPath)
242
+ const to = displayPath(row.file.newPath)
243
+ const expandable = !row.file.binary && row.file.hunks.length > 0
244
+ const fileExpanded = expandedFiles.has(row.fileIndex)
245
+ return (
246
+ <button
247
+ key={row.key}
248
+ type="button"
249
+ className={css.gitDiffFile}
250
+ disabled={!expandable}
251
+ aria-expanded={expandable ? fileExpanded : undefined}
252
+ onClick={() => {
253
+ setExpandedFiles(current => {
254
+ const next = new Set(current)
255
+ if (next.has(row.fileIndex)) next.delete(row.fileIndex)
256
+ else next.add(row.fileIndex)
257
+ return next
258
+ })
259
+ }}
260
+ >
261
+ {expandable && <span aria-hidden="true" className={clsx(css.gitDiffFileChevron, fileExpanded && css.gitDiffFileChevronExpanded)}>›</span>}
262
+ <span className={css.gitDiffFilePath}>{to}</span>
263
+ {from !== to && <span className={css.gitDiffFileOld}>← {from}</span>}
264
+ {tag !== null && <span className={css.gitDiffFileTag}>{tag}</span>}
265
+ </button>
266
+ )
267
+ }
268
+ if (row.type === 'hunk') {
269
+ const hunk = row.hunk!
270
+ return (
271
+ <div key={row.key} className={css.gitDiffHunk}>
272
+ <span className={css.gitDiffHunkHeader}>@@ -{hunk.oldStart},{hunk.lines.filter(l => l.oldNum !== null).length} +{hunk.newStart},{hunk.lines.filter(l => l.newNum !== null).length} @@</span>
273
+ {hunk.header !== '' && <span className={css.gitDiffHunkSection}>{hunk.header}</span>}
274
+ </div>
275
+ )
276
+ }
277
+ const line = row.line!
278
+ const lineClass = line.kind === 'del' ? css.gitDiffDel : line.kind === 'add' ? css.gitDiffAdd : line.kind === 'meta' ? css.gitDiffMeta : css.gitDiffCtx
279
+ return (
280
+ <div key={row.key} className={clsx(css.gitDiffLine, lineClass)}>
281
+ {line.kind === 'meta'
282
+ ? <span className={css.gitDiffMetaText}>{line.text}</span>
283
+ : (
284
+ <>
285
+ <span className={css.gitDiffNum}>{line.oldNum ?? ''}</span>
286
+ <span className={css.gitDiffNum}>{line.newNum ?? ''}</span>
287
+ <span className={css.gitDiffCode}>{line.text}</span>
288
+ </>
289
+ )}
290
+ </div>
291
+ )
292
+ }
293
+
294
+ return (
295
+ <div className={css.gitDiff}>
296
+ {head.map(renderRow)}
297
+ {hidden > 0 && (
298
+ <button type="button" className={css.gitDiffExpand} aria-expanded={expanded} onClick={() => { setExpanded(value => !value) }}>
299
+ {expanded ? t('diffCollapse') : t('diffExpand', { count: hidden })}
300
+ </button>
301
+ )}
302
+ {tail.map(renderRow)}
303
+ </div>
304
+ )
305
+ }