nastechai-desktop 18.1.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 (546) hide show
  1. package/.prettierrc +11 -0
  2. package/DESIGN.md +167 -0
  3. package/README.md +141 -0
  4. package/assets/icon.icns +0 -0
  5. package/assets/icon.ico +0 -0
  6. package/assets/icon.png +0 -0
  7. package/components.json +21 -0
  8. package/electron/backend-env.cjs +112 -0
  9. package/electron/backend-env.test.cjs +111 -0
  10. package/electron/backend-probes.cjs +106 -0
  11. package/electron/backend-probes.test.cjs +82 -0
  12. package/electron/backend-ready.cjs +66 -0
  13. package/electron/bootstrap-platform.cjs +91 -0
  14. package/electron/bootstrap-platform.test.cjs +111 -0
  15. package/electron/bootstrap-runner.cjs +720 -0
  16. package/electron/bootstrap-runner.test.cjs +138 -0
  17. package/electron/connection-config.cjs +254 -0
  18. package/electron/connection-config.test.cjs +329 -0
  19. package/electron/dashboard-token.cjs +99 -0
  20. package/electron/dashboard-token.test.cjs +142 -0
  21. package/electron/desktop-uninstall.cjs +232 -0
  22. package/electron/desktop-uninstall.test.cjs +246 -0
  23. package/electron/entitlements.mac.inherit.plist +14 -0
  24. package/electron/entitlements.mac.plist +14 -0
  25. package/electron/fs-read-dir.cjs +109 -0
  26. package/electron/fs-read-dir.test.cjs +364 -0
  27. package/electron/gateway-ws-probe.cjs +188 -0
  28. package/electron/gateway-ws-probe.test.cjs +122 -0
  29. package/electron/git-root.cjs +54 -0
  30. package/electron/git-root.test.cjs +40 -0
  31. package/electron/git-worktrees.cjs +174 -0
  32. package/electron/hardening.cjs +184 -0
  33. package/electron/hardening.test.cjs +116 -0
  34. package/electron/main.cjs +5762 -0
  35. package/electron/oauth-net-request.cjs +20 -0
  36. package/electron/oauth-net-request.test.cjs +34 -0
  37. package/electron/preload.cjs +135 -0
  38. package/electron/session-windows.cjs +99 -0
  39. package/electron/session-windows.test.cjs +177 -0
  40. package/electron/update-remote.cjs +56 -0
  41. package/electron/update-remote.test.cjs +78 -0
  42. package/electron/vscode-marketplace.cjs +331 -0
  43. package/electron/vscode-marketplace.test.cjs +113 -0
  44. package/electron/windows-child-process.test.cjs +57 -0
  45. package/electron/windows-user-env.cjs +76 -0
  46. package/electron/windows-user-env.test.cjs +90 -0
  47. package/electron/workspace-cwd.cjs +38 -0
  48. package/electron/workspace-cwd.test.cjs +45 -0
  49. package/eslint.config.mjs +122 -0
  50. package/index.html +17 -0
  51. package/package.json +254 -0
  52. package/pr-assets/session-source-folders.png +0 -0
  53. package/preview-demo.html +65 -0
  54. package/public/apple-touch-icon.png +0 -0
  55. package/public/ds-assets/filler-bg0.jpg +0 -0
  56. package/public/nastech-frames/nastech-frame-0.png +0 -0
  57. package/public/nastech-frames/nastech-frame-1.png +0 -0
  58. package/public/nastech-frames/nastech-frame-2.png +0 -0
  59. package/public/nastech-frames/nastech-frame-3.png +0 -0
  60. package/public/nastech-frames/nastech-frame-4.png +0 -0
  61. package/public/nastech-frames/nastech-frame-5.png +0 -0
  62. package/public/nastech-frames/nastech-frame-6.png +0 -0
  63. package/public/nastech-frames/nastech-frame-7.png +0 -0
  64. package/public/nastech-girl.jpg +0 -0
  65. package/public/nastech-sprite.png +0 -0
  66. package/public/nastech.png +0 -0
  67. package/scripts/after-pack.cjs +41 -0
  68. package/scripts/assert-dist-built.cjs +70 -0
  69. package/scripts/assert-dist-built.test.cjs +84 -0
  70. package/scripts/assert-root-install.cjs +13 -0
  71. package/scripts/before-build.cjs +11 -0
  72. package/scripts/before-pack.cjs +78 -0
  73. package/scripts/before-pack.test.cjs +53 -0
  74. package/scripts/click-session.mjs +51 -0
  75. package/scripts/dev-no-hmr.mjs +22 -0
  76. package/scripts/diag-jump.mjs +115 -0
  77. package/scripts/diag-scroll-reset.mjs +229 -0
  78. package/scripts/eval.mjs +21 -0
  79. package/scripts/leak-typing.mjs +222 -0
  80. package/scripts/measure-jump.mjs +108 -0
  81. package/scripts/measure-latency.mjs +184 -0
  82. package/scripts/measure-real-stream.mjs +252 -0
  83. package/scripts/measure-submit.mjs +179 -0
  84. package/scripts/measure-synthetic-stream.mjs +322 -0
  85. package/scripts/notarize-artifact.cjs +77 -0
  86. package/scripts/notarize.cjs +100 -0
  87. package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
  88. package/scripts/probe-renderer.mjs +38 -0
  89. package/scripts/probe-thread.mjs +40 -0
  90. package/scripts/profile-long-stream.mjs +191 -0
  91. package/scripts/profile-real-stream.mjs +137 -0
  92. package/scripts/profile-synth-stream.mjs +103 -0
  93. package/scripts/profile-typing-lag.md +381 -0
  94. package/scripts/profile-typing.mjs +260 -0
  95. package/scripts/reload-renderer.mjs +25 -0
  96. package/scripts/reload.mjs +36 -0
  97. package/scripts/set-exe-identity.cjs +94 -0
  98. package/scripts/stage-native-deps.cjs +159 -0
  99. package/scripts/test-desktop.mjs +425 -0
  100. package/scripts/write-build-stamp.cjs +126 -0
  101. package/src/app/agents/index.tsx +398 -0
  102. package/src/app/artifacts/index.test.ts +62 -0
  103. package/src/app/artifacts/index.tsx +906 -0
  104. package/src/app/chat/chat-drop-overlay.tsx +48 -0
  105. package/src/app/chat/chat-swap-overlay.tsx +47 -0
  106. package/src/app/chat/composer/attachments.tsx +114 -0
  107. package/src/app/chat/composer/completion-drawer.tsx +63 -0
  108. package/src/app/chat/composer/context-menu.tsx +172 -0
  109. package/src/app/chat/composer/controls.tsx +289 -0
  110. package/src/app/chat/composer/drop-affordance.ts +2 -0
  111. package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  112. package/src/app/chat/composer/focus.ts +134 -0
  113. package/src/app/chat/composer/help-hint.tsx +59 -0
  114. package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
  115. package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
  116. package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
  117. package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
  118. package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
  119. package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
  120. package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  121. package/src/app/chat/composer/index.tsx +1611 -0
  122. package/src/app/chat/composer/inline-refs.ts +138 -0
  123. package/src/app/chat/composer/model-pill.tsx +86 -0
  124. package/src/app/chat/composer/queue-panel.tsx +130 -0
  125. package/src/app/chat/composer/rich-editor.test.ts +18 -0
  126. package/src/app/chat/composer/rich-editor.ts +165 -0
  127. package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
  128. package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  129. package/src/app/chat/composer/status-stack/index.tsx +202 -0
  130. package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  131. package/src/app/chat/composer/text-utils.test.ts +77 -0
  132. package/src/app/chat/composer/text-utils.ts +107 -0
  133. package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
  134. package/src/app/chat/composer/trigger-popover.tsx +116 -0
  135. package/src/app/chat/composer/types.ts +64 -0
  136. package/src/app/chat/composer/url-dialog.tsx +82 -0
  137. package/src/app/chat/composer/voice-activity.tsx +252 -0
  138. package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  139. package/src/app/chat/hooks/use-composer-actions.ts +525 -0
  140. package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
  141. package/src/app/chat/index.tsx +390 -0
  142. package/src/app/chat/perf-probe.tsx +269 -0
  143. package/src/app/chat/right-rail/index.ts +1 -0
  144. package/src/app/chat/right-rail/preview-console-state.ts +82 -0
  145. package/src/app/chat/right-rail/preview-console.tsx +290 -0
  146. package/src/app/chat/right-rail/preview-file.tsx +559 -0
  147. package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
  148. package/src/app/chat/right-rail/preview-pane.tsx +657 -0
  149. package/src/app/chat/right-rail/preview.tsx +171 -0
  150. package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  151. package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  152. package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
  153. package/src/app/chat/sidebar/index.tsx +1219 -0
  154. package/src/app/chat/sidebar/load-more-row.tsx +30 -0
  155. package/src/app/chat/sidebar/order.test.ts +21 -0
  156. package/src/app/chat/sidebar/order.ts +17 -0
  157. package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
  158. package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
  159. package/src/app/chat/sidebar/session-row.tsx +257 -0
  160. package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
  161. package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  162. package/src/app/chat/sidebar/workspace-groups.ts +326 -0
  163. package/src/app/chat/thread-loading.test.ts +34 -0
  164. package/src/app/chat/thread-loading.ts +26 -0
  165. package/src/app/command-center/index.tsx +654 -0
  166. package/src/app/command-palette/index.tsx +513 -0
  167. package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  168. package/src/app/cron/index.tsx +942 -0
  169. package/src/app/cron/job-state.ts +29 -0
  170. package/src/app/desktop-controller.tsx +938 -0
  171. package/src/app/floating-hud.ts +22 -0
  172. package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  173. package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
  174. package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
  175. package/src/app/hooks/use-keybinds.ts +186 -0
  176. package/src/app/hooks/use-refresh-hotkey.ts +45 -0
  177. package/src/app/hooks/use-route-enum-param.ts +38 -0
  178. package/src/app/index.tsx +1 -0
  179. package/src/app/layout-constants.ts +13 -0
  180. package/src/app/messaging/index.tsx +648 -0
  181. package/src/app/messaging/platform-icon.tsx +93 -0
  182. package/src/app/model-picker-overlay.tsx +42 -0
  183. package/src/app/model-visibility-overlay.tsx +31 -0
  184. package/src/app/overlays/overlay-chrome.tsx +66 -0
  185. package/src/app/overlays/overlay-search-input.tsx +33 -0
  186. package/src/app/overlays/overlay-split-layout.tsx +130 -0
  187. package/src/app/overlays/overlay-view.tsx +91 -0
  188. package/src/app/page-search-shell.tsx +75 -0
  189. package/src/app/profiles/create-profile-dialog.tsx +154 -0
  190. package/src/app/profiles/delete-profile-dialog.tsx +65 -0
  191. package/src/app/profiles/index.tsx +671 -0
  192. package/src/app/profiles/rename-profile-dialog.tsx +125 -0
  193. package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  194. package/src/app/right-sidebar/files/ipc.test.ts +100 -0
  195. package/src/app/right-sidebar/files/ipc.ts +161 -0
  196. package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  197. package/src/app/right-sidebar/files/tree.tsx +224 -0
  198. package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
  199. package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
  200. package/src/app/right-sidebar/index.test.tsx +75 -0
  201. package/src/app/right-sidebar/index.tsx +395 -0
  202. package/src/app/right-sidebar/store.ts +15 -0
  203. package/src/app/right-sidebar/terminal/buffer.ts +65 -0
  204. package/src/app/right-sidebar/terminal/index.tsx +98 -0
  205. package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
  206. package/src/app/right-sidebar/terminal/selection.ts +75 -0
  207. package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
  208. package/src/app/routes.ts +88 -0
  209. package/src/app/session/hooks/use-context-suggestions.ts +58 -0
  210. package/src/app/session/hooks/use-cwd-actions.ts +109 -0
  211. package/src/app/session/hooks/use-message-stream.ts +957 -0
  212. package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  213. package/src/app/session/hooks/use-model-controls.ts +106 -0
  214. package/src/app/session/hooks/use-nastech-config.ts +74 -0
  215. package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
  216. package/src/app/session/hooks/use-preview-routing.ts +223 -0
  217. package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
  218. package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
  219. package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
  220. package/src/app/session/hooks/use-route-resume.ts +115 -0
  221. package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
  222. package/src/app/session/hooks/use-session-actions.ts +885 -0
  223. package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
  224. package/src/app/session/hooks/use-session-state-cache.ts +191 -0
  225. package/src/app/session-picker-overlay.tsx +32 -0
  226. package/src/app/session-switcher.tsx +107 -0
  227. package/src/app/settings/about-settings.tsx +173 -0
  228. package/src/app/settings/appearance-settings.tsx +162 -0
  229. package/src/app/settings/config-settings.tsx +384 -0
  230. package/src/app/settings/constants.ts +545 -0
  231. package/src/app/settings/credential-key-ui.tsx +373 -0
  232. package/src/app/settings/env-credentials.tsx +198 -0
  233. package/src/app/settings/env-var-actions-menu.tsx +136 -0
  234. package/src/app/settings/field-copy.ts +56 -0
  235. package/src/app/settings/gateway-settings.tsx +620 -0
  236. package/src/app/settings/helpers.test.ts +138 -0
  237. package/src/app/settings/helpers.ts +151 -0
  238. package/src/app/settings/index.tsx +237 -0
  239. package/src/app/settings/keys-settings.tsx +96 -0
  240. package/src/app/settings/mcp-settings.tsx +271 -0
  241. package/src/app/settings/model-settings.test.tsx +157 -0
  242. package/src/app/settings/model-settings.tsx +559 -0
  243. package/src/app/settings/notifications-settings.tsx +150 -0
  244. package/src/app/settings/primitives.tsx +115 -0
  245. package/src/app/settings/providers-settings.test.tsx +100 -0
  246. package/src/app/settings/providers-settings.tsx +258 -0
  247. package/src/app/settings/sessions-settings.tsx +276 -0
  248. package/src/app/settings/toolset-config-panel.test.tsx +289 -0
  249. package/src/app/settings/toolset-config-panel.tsx +449 -0
  250. package/src/app/settings/types.ts +42 -0
  251. package/src/app/settings/uninstall-section.tsx +185 -0
  252. package/src/app/settings/use-deep-link-highlight.ts +60 -0
  253. package/src/app/shell/app-shell.tsx +167 -0
  254. package/src/app/shell/gateway-menu-panel.tsx +150 -0
  255. package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
  256. package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
  257. package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
  258. package/src/app/shell/keybind-panel.tsx +220 -0
  259. package/src/app/shell/model-edit-submenu.test.tsx +84 -0
  260. package/src/app/shell/model-edit-submenu.tsx +245 -0
  261. package/src/app/shell/model-menu-panel.tsx +295 -0
  262. package/src/app/shell/sidebar-label.tsx +22 -0
  263. package/src/app/shell/statusbar-controls.tsx +185 -0
  264. package/src/app/shell/titlebar-controls.tsx +244 -0
  265. package/src/app/shell/titlebar.test.ts +26 -0
  266. package/src/app/shell/titlebar.ts +45 -0
  267. package/src/app/shell/use-group-registry.ts +39 -0
  268. package/src/app/skills/index.test.tsx +103 -0
  269. package/src/app/skills/index.tsx +371 -0
  270. package/src/app/types.ts +99 -0
  271. package/src/app/updates-overlay.tsx +369 -0
  272. package/src/components/Backdrop.tsx +114 -0
  273. package/src/components/assistant-ui/ansi-text.tsx +34 -0
  274. package/src/components/assistant-ui/clarify-tool.tsx +281 -0
  275. package/src/components/assistant-ui/directive-text.test.ts +39 -0
  276. package/src/components/assistant-ui/directive-text.tsx +389 -0
  277. package/src/components/assistant-ui/markdown-text.test.ts +204 -0
  278. package/src/components/assistant-ui/markdown-text.tsx +497 -0
  279. package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  280. package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  281. package/src/components/assistant-ui/streaming.test.tsx +739 -0
  282. package/src/components/assistant-ui/thread-list.tsx +307 -0
  283. package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
  284. package/src/components/assistant-ui/thread.tsx +1474 -0
  285. package/src/components/assistant-ui/todo-tool.tsx +109 -0
  286. package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
  287. package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
  288. package/src/components/assistant-ui/tool-approval.tsx +209 -0
  289. package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
  290. package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
  291. package/src/components/assistant-ui/tool-fallback.tsx +466 -0
  292. package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
  293. package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  294. package/src/components/assistant-ui/user-message-text.tsx +150 -0
  295. package/src/components/boot-failure-overlay.tsx +246 -0
  296. package/src/components/boot-failure-reauth.test.ts +100 -0
  297. package/src/components/boot-failure-reauth.ts +81 -0
  298. package/src/components/brand-mark.tsx +19 -0
  299. package/src/components/chat/activity-timer-text.tsx +24 -0
  300. package/src/components/chat/activity-timer.test.tsx +43 -0
  301. package/src/components/chat/activity-timer.ts +64 -0
  302. package/src/components/chat/code-card.tsx +78 -0
  303. package/src/components/chat/compact-markdown.tsx +113 -0
  304. package/src/components/chat/composer-dock.ts +31 -0
  305. package/src/components/chat/diff-lines.tsx +54 -0
  306. package/src/components/chat/disclosure-row.tsx +63 -0
  307. package/src/components/chat/generated-image-context.tsx +19 -0
  308. package/src/components/chat/generated-image-result.tsx +174 -0
  309. package/src/components/chat/image-generation-placeholder.tsx +279 -0
  310. package/src/components/chat/intro-copy.jsonl +75 -0
  311. package/src/components/chat/intro.tsx +182 -0
  312. package/src/components/chat/preview-attachment.tsx +125 -0
  313. package/src/components/chat/shiki-highlighter.tsx +107 -0
  314. package/src/components/chat/status-row.tsx +70 -0
  315. package/src/components/chat/status-section.tsx +42 -0
  316. package/src/components/chat/terminal-output.tsx +50 -0
  317. package/src/components/chat/zoomable-image.tsx +177 -0
  318. package/src/components/desktop-install-overlay.tsx +595 -0
  319. package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
  320. package/src/components/desktop-onboarding-overlay.tsx +1286 -0
  321. package/src/components/error-boundary.tsx +77 -0
  322. package/src/components/gateway-connecting-overlay.test.tsx +143 -0
  323. package/src/components/gateway-connecting-overlay.tsx +183 -0
  324. package/src/components/haptics-provider.tsx +19 -0
  325. package/src/components/language-switcher.test.tsx +53 -0
  326. package/src/components/language-switcher.tsx +175 -0
  327. package/src/components/model-picker.tsx +340 -0
  328. package/src/components/model-visibility-dialog.tsx +155 -0
  329. package/src/components/notifications.tsx +196 -0
  330. package/src/components/page-loader.tsx +34 -0
  331. package/src/components/pane-shell/context.ts +14 -0
  332. package/src/components/pane-shell/index.ts +4 -0
  333. package/src/components/pane-shell/pane-shell.test.tsx +333 -0
  334. package/src/components/pane-shell/pane-shell.tsx +330 -0
  335. package/src/components/prompt-overlays.tsx +234 -0
  336. package/src/components/session-picker.tsx +108 -0
  337. package/src/components/status-dot.tsx +26 -0
  338. package/src/components/ui/action-status.tsx +25 -0
  339. package/src/components/ui/alert.tsx +53 -0
  340. package/src/components/ui/badge.tsx +35 -0
  341. package/src/components/ui/braille-spinner.tsx +61 -0
  342. package/src/components/ui/button.tsx +81 -0
  343. package/src/components/ui/checkbox.tsx +27 -0
  344. package/src/components/ui/codicon.tsx +20 -0
  345. package/src/components/ui/command.tsx +111 -0
  346. package/src/components/ui/confirm-dialog.tsx +109 -0
  347. package/src/components/ui/context-menu.tsx +141 -0
  348. package/src/components/ui/control.ts +25 -0
  349. package/src/components/ui/copy-button.test.tsx +36 -0
  350. package/src/components/ui/copy-button.tsx +229 -0
  351. package/src/components/ui/dialog.tsx +152 -0
  352. package/src/components/ui/disclosure-caret.tsx +20 -0
  353. package/src/components/ui/dropdown-menu.tsx +291 -0
  354. package/src/components/ui/error-state.tsx +50 -0
  355. package/src/components/ui/fade-text.tsx +110 -0
  356. package/src/components/ui/glyph-spinner.tsx +63 -0
  357. package/src/components/ui/input.tsx +22 -0
  358. package/src/components/ui/kbd.tsx +37 -0
  359. package/src/components/ui/loader.tsx +558 -0
  360. package/src/components/ui/log-view.tsx +17 -0
  361. package/src/components/ui/pagination.tsx +114 -0
  362. package/src/components/ui/popover.tsx +44 -0
  363. package/src/components/ui/scroll-area.tsx +43 -0
  364. package/src/components/ui/search-field.tsx +80 -0
  365. package/src/components/ui/segmented-control.tsx +51 -0
  366. package/src/components/ui/select.tsx +92 -0
  367. package/src/components/ui/separator.tsx +26 -0
  368. package/src/components/ui/sheet.tsx +116 -0
  369. package/src/components/ui/sidebar.tsx +674 -0
  370. package/src/components/ui/skeleton.tsx +7 -0
  371. package/src/components/ui/switch.tsx +49 -0
  372. package/src/components/ui/tabs.tsx +36 -0
  373. package/src/components/ui/text-tab.tsx +43 -0
  374. package/src/components/ui/textarea.tsx +11 -0
  375. package/src/components/ui/tool-icon.tsx +65 -0
  376. package/src/components/ui/tooltip.tsx +69 -0
  377. package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/src/global.d.ts +457 -0
  381. package/src/hooks/use-image-download.ts +85 -0
  382. package/src/hooks/use-media-query.ts +24 -0
  383. package/src/hooks/use-mobile.ts +3 -0
  384. package/src/hooks/use-resize-observer.ts +38 -0
  385. package/src/hooks/use-worktree-info.ts +68 -0
  386. package/src/i18n/catalog.ts +12 -0
  387. package/src/i18n/context.test.tsx +232 -0
  388. package/src/i18n/context.tsx +183 -0
  389. package/src/i18n/define-locale.ts +41 -0
  390. package/src/i18n/en.ts +1779 -0
  391. package/src/i18n/index.ts +20 -0
  392. package/src/i18n/ja.ts +1890 -0
  393. package/src/i18n/languages.test.ts +43 -0
  394. package/src/i18n/languages.ts +86 -0
  395. package/src/i18n/runtime.test.ts +75 -0
  396. package/src/i18n/runtime.ts +53 -0
  397. package/src/i18n/types.ts +1452 -0
  398. package/src/i18n/zh-hant.ts +1849 -0
  399. package/src/i18n/zh.ts +1923 -0
  400. package/src/lib/ansi.test.ts +123 -0
  401. package/src/lib/ansi.ts +175 -0
  402. package/src/lib/chat-messages.test.ts +708 -0
  403. package/src/lib/chat-messages.ts +885 -0
  404. package/src/lib/chat-runtime.test.ts +18 -0
  405. package/src/lib/chat-runtime.ts +335 -0
  406. package/src/lib/clipboard.ts +28 -0
  407. package/src/lib/commit-changelog.test.ts +114 -0
  408. package/src/lib/commit-changelog.ts +177 -0
  409. package/src/lib/completion-sound.ts +519 -0
  410. package/src/lib/desktop-fs.test.ts +116 -0
  411. package/src/lib/desktop-fs.ts +113 -0
  412. package/src/lib/desktop-slash-commands.test.ts +126 -0
  413. package/src/lib/desktop-slash-commands.ts +286 -0
  414. package/src/lib/embedded-images.test.ts +35 -0
  415. package/src/lib/embedded-images.ts +60 -0
  416. package/src/lib/external-link.test.tsx +168 -0
  417. package/src/lib/external-link.tsx +303 -0
  418. package/src/lib/gateway-events.test.ts +27 -0
  419. package/src/lib/gateway-events.ts +49 -0
  420. package/src/lib/gateway-ws-url.test.ts +78 -0
  421. package/src/lib/gateway-ws-url.ts +91 -0
  422. package/src/lib/generated-images.test.ts +97 -0
  423. package/src/lib/generated-images.ts +116 -0
  424. package/src/lib/haptics.ts +129 -0
  425. package/src/lib/icons.ts +203 -0
  426. package/src/lib/incremental-external-store-runtime.ts +188 -0
  427. package/src/lib/katex-memo.ts +260 -0
  428. package/src/lib/keybinds/actions.ts +125 -0
  429. package/src/lib/keybinds/combo.test.ts +86 -0
  430. package/src/lib/keybinds/combo.ts +169 -0
  431. package/src/lib/local-preview.ts +126 -0
  432. package/src/lib/markdown-code.test.ts +23 -0
  433. package/src/lib/markdown-code.ts +195 -0
  434. package/src/lib/markdown-preprocess.ts +386 -0
  435. package/src/lib/media.remote.test.ts +58 -0
  436. package/src/lib/media.ts +111 -0
  437. package/src/lib/model-status-label.test.ts +31 -0
  438. package/src/lib/model-status-label.ts +103 -0
  439. package/src/lib/mutable-ref.ts +6 -0
  440. package/src/lib/preview-targets.test.ts +27 -0
  441. package/src/lib/preview-targets.ts +63 -0
  442. package/src/lib/profile-color.ts +58 -0
  443. package/src/lib/provider-setup-errors.test.ts +26 -0
  444. package/src/lib/provider-setup-errors.ts +12 -0
  445. package/src/lib/query-client.ts +13 -0
  446. package/src/lib/remend-tail.test.ts +105 -0
  447. package/src/lib/remend-tail.ts +108 -0
  448. package/src/lib/runtime-readiness.test.ts +65 -0
  449. package/src/lib/runtime-readiness.ts +147 -0
  450. package/src/lib/session-export.ts +57 -0
  451. package/src/lib/session-search.test.ts +58 -0
  452. package/src/lib/session-search.ts +19 -0
  453. package/src/lib/session-source.ts +62 -0
  454. package/src/lib/speech-text.ts +35 -0
  455. package/src/lib/statusbar.ts +91 -0
  456. package/src/lib/storage.test.ts +25 -0
  457. package/src/lib/storage.ts +107 -0
  458. package/src/lib/todos.test.ts +35 -0
  459. package/src/lib/todos.ts +51 -0
  460. package/src/lib/tool-result-summary.test.ts +106 -0
  461. package/src/lib/tool-result-summary.ts +467 -0
  462. package/src/lib/update-copy.test.ts +38 -0
  463. package/src/lib/update-copy.ts +44 -0
  464. package/src/lib/use-enter-animation.ts +100 -0
  465. package/src/lib/utils.ts +6 -0
  466. package/src/lib/voice-playback.ts +128 -0
  467. package/src/lib/yolo-session.ts +26 -0
  468. package/src/main.tsx +43 -0
  469. package/src/nastech.test.ts +49 -0
  470. package/src/nastech.ts +718 -0
  471. package/src/store/activity.ts +100 -0
  472. package/src/store/boot.ts +91 -0
  473. package/src/store/clarify.test.ts +81 -0
  474. package/src/store/clarify.ts +69 -0
  475. package/src/store/command-palette.ts +20 -0
  476. package/src/store/compaction.test.ts +53 -0
  477. package/src/store/compaction.ts +38 -0
  478. package/src/store/completion-sound.ts +32 -0
  479. package/src/store/composer-input-history.test.ts +147 -0
  480. package/src/store/composer-input-history.ts +158 -0
  481. package/src/store/composer-queue.test.ts +148 -0
  482. package/src/store/composer-queue.ts +239 -0
  483. package/src/store/composer-status.test.ts +99 -0
  484. package/src/store/composer-status.ts +277 -0
  485. package/src/store/composer.test.ts +106 -0
  486. package/src/store/composer.ts +184 -0
  487. package/src/store/cron.ts +19 -0
  488. package/src/store/gateway.ts +290 -0
  489. package/src/store/haptics.ts +17 -0
  490. package/src/store/keybinds.ts +139 -0
  491. package/src/store/layout.ts +176 -0
  492. package/src/store/model-presets.test.ts +51 -0
  493. package/src/store/model-presets.ts +86 -0
  494. package/src/store/model-visibility.test.ts +37 -0
  495. package/src/store/model-visibility.ts +108 -0
  496. package/src/store/native-notifications.test.ts +192 -0
  497. package/src/store/native-notifications.ts +203 -0
  498. package/src/store/notifications.ts +165 -0
  499. package/src/store/onboarding.test.ts +372 -0
  500. package/src/store/onboarding.ts +866 -0
  501. package/src/store/panes.test.ts +146 -0
  502. package/src/store/panes.ts +145 -0
  503. package/src/store/preview.test.ts +135 -0
  504. package/src/store/preview.ts +466 -0
  505. package/src/store/profile.test.ts +89 -0
  506. package/src/store/profile.ts +365 -0
  507. package/src/store/prompts.test.ts +121 -0
  508. package/src/store/prompts.ts +115 -0
  509. package/src/store/session-switcher.test.ts +115 -0
  510. package/src/store/session-switcher.ts +128 -0
  511. package/src/store/session-sync.ts +25 -0
  512. package/src/store/session.test.ts +131 -0
  513. package/src/store/session.ts +255 -0
  514. package/src/store/subagents.test.ts +111 -0
  515. package/src/store/subagents.ts +260 -0
  516. package/src/store/thread-scroll.ts +46 -0
  517. package/src/store/todos.test.ts +47 -0
  518. package/src/store/todos.ts +64 -0
  519. package/src/store/tool-diffs.ts +23 -0
  520. package/src/store/tool-dismiss.ts +45 -0
  521. package/src/store/tool-view.ts +91 -0
  522. package/src/store/translucency.ts +38 -0
  523. package/src/store/updates.test.ts +77 -0
  524. package/src/store/updates.ts +315 -0
  525. package/src/store/voice-playback.ts +24 -0
  526. package/src/store/windows.test.ts +143 -0
  527. package/src/store/windows.ts +77 -0
  528. package/src/styles.css +1235 -0
  529. package/src/themes/color.ts +142 -0
  530. package/src/themes/context.tsx +339 -0
  531. package/src/themes/index.ts +3 -0
  532. package/src/themes/install.test.ts +119 -0
  533. package/src/themes/install.ts +95 -0
  534. package/src/themes/presets.test.ts +33 -0
  535. package/src/themes/presets.ts +293 -0
  536. package/src/themes/profile-theme.test.ts +41 -0
  537. package/src/themes/types.ts +66 -0
  538. package/src/themes/use-skin-command.ts +60 -0
  539. package/src/themes/user-themes.test.ts +63 -0
  540. package/src/themes/user-themes.ts +122 -0
  541. package/src/themes/vscode.test.ts +171 -0
  542. package/src/themes/vscode.ts +343 -0
  543. package/src/types/nastech.ts +646 -0
  544. package/src/vite-env.d.ts +1 -0
  545. package/tsconfig.json +25 -0
  546. package/vite.config.ts +56 -0
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { coerceThinkingText } from './chat-runtime'
4
+
5
+ describe('coerceThinkingText', () => {
6
+ it('strips streaming status prefixes from thinking deltas', () => {
7
+ expect(coerceThinkingText("◉_◉ processing... checking the user's request")).toBe("checking the user's request")
8
+ expect(coerceThinkingText('(¬‿¬) analyzing... reading the file')).toBe('reading the file')
9
+ })
10
+
11
+ it('drops empty thinking rewrite placeholder text', () => {
12
+ expect(
13
+ coerceThinkingText(
14
+ "◉_◉ processing... I don't see any current rewritten thinking or next thinking to process. Could you provide the thinking content you'd like me to rewrite?"
15
+ )
16
+ ).toBe('')
17
+ })
18
+ })
@@ -0,0 +1,335 @@
1
+ import type { ThreadMessage } from '@assistant-ui/react'
2
+
3
+ import type { QuickModelOption } from '@/app/chat/composer/types'
4
+ import type { ClientSessionState, CommandDispatchResponse } from '@/app/types'
5
+ import { formatRefValue } from '@/components/assistant-ui/directive-text'
6
+ import { type ChatMessage, type ChatMessagePart, chatMessageText, textPart } from '@/lib/chat-messages'
7
+ import type { ComposerAttachment } from '@/store/composer'
8
+ import type { ModelOptionsResponse, SessionInfo } from '@/types/nastech'
9
+
10
+ export const SLASH_COMMAND_RE = /^\/[^\s/]*(?:\s|$)/
11
+ export const BUILTIN_PERSONALITIES = [
12
+ 'helpful',
13
+ 'concise',
14
+ 'technical',
15
+ 'creative',
16
+ 'teacher',
17
+ 'kawaii',
18
+ 'catgirl',
19
+ 'pirate',
20
+ 'shakespeare',
21
+ 'surfer',
22
+ 'noir',
23
+ 'uwu',
24
+ 'philosopher',
25
+ 'hype'
26
+ ]
27
+
28
+ const THINKING_STATUS_PREFIX_RE =
29
+ /^\s*(?:(?:[^\s.]{1,16})\s+)?(?:processing|thinking|reasoning|analyzing|pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|computing|synthesizing|formulating|brainstorming)\.\.\.\s*/i
30
+
31
+ const EMPTY_THINKING_PLACEHOLDER_RE =
32
+ /\b(?:current rewritten thinking|next thinking to process|provide the thinking content|don't see any .*thinking)\b/i
33
+
34
+ export function createClientSessionState(
35
+ storedSessionId: string | null = null,
36
+ messages: ChatMessage[] = []
37
+ ): ClientSessionState {
38
+ return {
39
+ storedSessionId,
40
+ messages,
41
+ branch: '',
42
+ cwd: '',
43
+ busy: false,
44
+ awaitingResponse: false,
45
+ streamId: null,
46
+ sawAssistantPayload: false,
47
+ pendingBranchGroup: null,
48
+ interrupted: false,
49
+ needsInput: false,
50
+ turnStartedAt: null
51
+ }
52
+ }
53
+
54
+ export function sessionTitle(session: SessionInfo): string {
55
+ return session.title?.trim() || session.preview?.trim() || 'Untitled session'
56
+ }
57
+
58
+ export function coerceGatewayText(value: unknown): string {
59
+ if (typeof value === 'string') {
60
+ return value
61
+ }
62
+
63
+ if (value === null || value === undefined) {
64
+ return ''
65
+ }
66
+
67
+ if (Array.isArray(value)) {
68
+ return value
69
+ .map(item => {
70
+ if (typeof item === 'string') {
71
+ return item
72
+ }
73
+
74
+ if (item && typeof item === 'object') {
75
+ const row = item as Record<string, unknown>
76
+
77
+ if (typeof row.text === 'string') {
78
+ return row.text
79
+ }
80
+
81
+ if (typeof row.output_text === 'string') {
82
+ return row.output_text
83
+ }
84
+ }
85
+
86
+ return ''
87
+ })
88
+ .join('')
89
+ }
90
+
91
+ if (typeof value === 'object') {
92
+ const row = value as Record<string, unknown>
93
+
94
+ if (typeof row.text === 'string') {
95
+ return row.text
96
+ }
97
+
98
+ if (typeof row.output_text === 'string') {
99
+ return row.output_text
100
+ }
101
+
102
+ try {
103
+ return JSON.stringify(value)
104
+ } catch {
105
+ return ''
106
+ }
107
+ }
108
+
109
+ return String(value)
110
+ }
111
+
112
+ /**
113
+ * Normalize a reasoning/thinking text payload from the gateway.
114
+ *
115
+ * Only the leading status prefix (e.g. "NasTech is thinking...") and the
116
+ * obvious placeholder echoes are stripped. We deliberately do NOT trim
117
+ * the delta — reasoning streams as small chunks (often individual tokens
118
+ * with leading or trailing spaces), and trimming each chunk before
119
+ * concatenation collapses adjacent words together. Whitespace between
120
+ * tokens belongs to the data, not chrome.
121
+ */
122
+ export function coerceThinkingText(value: unknown): string {
123
+ const raw = coerceGatewayText(value).replace(THINKING_STATUS_PREFIX_RE, '')
124
+
125
+ return EMPTY_THINKING_PLACEHOLDER_RE.test(raw) ? '' : raw
126
+ }
127
+
128
+ export function isImageGenerationTool(name?: string): boolean {
129
+ return name === 'image_generate'
130
+ }
131
+
132
+ export function contextPath(path: string, cwd: string): string {
133
+ if (!cwd) {
134
+ return path
135
+ }
136
+
137
+ const normalizedCwd = cwd.endsWith('/') ? cwd : `${cwd}/`
138
+
139
+ return path.startsWith(normalizedCwd) ? path.slice(normalizedCwd.length) : path
140
+ }
141
+
142
+ export function attachmentId(kind: ComposerAttachment['kind'], value: string): string {
143
+ return `${kind}:${value}`
144
+ }
145
+
146
+ export function pathLabel(path: string): string {
147
+ return path.split(/[\\/]/).filter(Boolean).pop() || path
148
+ }
149
+
150
+ export function attachmentDisplayText(attachment: ComposerAttachment): string | null {
151
+ if (attachment.kind === 'terminal' && attachment.detail) {
152
+ return `\`\`\`terminal\n${attachment.detail.trim()}\n\`\`\``
153
+ }
154
+
155
+ if (attachment.refText) {
156
+ return attachment.refText
157
+ }
158
+
159
+ if (attachment.kind === 'image') {
160
+ const id = attachment.detail || attachment.path || attachment.label
161
+
162
+ return id ? `@image:${formatRefValue(id)}` : null
163
+ }
164
+
165
+ return null
166
+ }
167
+
168
+ export function personalityNamesFromConfig(config: unknown): string[] {
169
+ const root = config && typeof config === 'object' ? (config as Record<string, unknown>) : {}
170
+ const agent = root.agent && typeof root.agent === 'object' ? (root.agent as Record<string, unknown>) : {}
171
+ const personalities = agent.personalities
172
+
173
+ return personalities && typeof personalities === 'object' && !Array.isArray(personalities)
174
+ ? Object.keys(personalities as Record<string, unknown>)
175
+ : []
176
+ }
177
+
178
+ export function normalizePersonalityValue(value: string): string {
179
+ const trimmed = value.trim().toLowerCase()
180
+
181
+ return !trimmed || trimmed === 'default' || trimmed === 'none' ? '' : trimmed
182
+ }
183
+
184
+ export function parseSlashCommand(command: string) {
185
+ const match = command.replace(/^\/+/, '').match(/^(\S+)\s*(.*)$/)
186
+
187
+ return match ? { name: match[1], arg: match[2].trim() } : { name: '', arg: '' }
188
+ }
189
+
190
+ export function parseCommandDispatch(raw: unknown): CommandDispatchResponse | null {
191
+ if (!raw || typeof raw !== 'object') {
192
+ return null
193
+ }
194
+
195
+ const row = raw as Record<string, unknown>
196
+ const str = (value: unknown) => (typeof value === 'string' ? value : undefined)
197
+
198
+ switch (row.type) {
199
+ case 'exec':
200
+
201
+ case 'plugin':
202
+ return { type: row.type, output: str(row.output) }
203
+
204
+ case 'alias':
205
+ return typeof row.target === 'string' ? { type: 'alias', target: row.target } : null
206
+
207
+ case 'skill':
208
+ return typeof row.name === 'string' ? { type: 'skill', name: row.name, message: str(row.message) } : null
209
+
210
+ case 'send':
211
+ return typeof row.message === 'string' ? { type: 'send', message: row.message } : null
212
+
213
+ default:
214
+ return null
215
+ }
216
+ }
217
+
218
+ export function quickModelOptions(
219
+ data: ModelOptionsResponse | undefined,
220
+ currentProvider: string,
221
+ currentModel: string
222
+ ): QuickModelOption[] {
223
+ const seen = new Set<string>()
224
+ const options: QuickModelOption[] = []
225
+
226
+ const providers = [...(data?.providers ?? [])].sort((a, b) => {
227
+ if (a.slug === currentProvider) {
228
+ return -1
229
+ }
230
+
231
+ if (b.slug === currentProvider) {
232
+ return 1
233
+ }
234
+
235
+ if (a.is_current) {
236
+ return -1
237
+ }
238
+
239
+ if (b.is_current) {
240
+ return 1
241
+ }
242
+
243
+ return 0
244
+ })
245
+
246
+ const add = (provider: string, providerName: string, model: string) => {
247
+ const key = `${provider}:${model}`
248
+
249
+ if (!model || seen.has(key)) {
250
+ return
251
+ }
252
+
253
+ seen.add(key)
254
+ options.push({ provider, providerName, model })
255
+ }
256
+
257
+ if (currentProvider && currentModel) {
258
+ add(currentProvider, currentProvider, currentModel)
259
+ }
260
+
261
+ for (const provider of providers) {
262
+ const models = [...(provider.models ?? [])].sort((a, b) => {
263
+ if (provider.slug === currentProvider && a === currentModel) {
264
+ return -1
265
+ }
266
+
267
+ if (provider.slug === currentProvider && b === currentModel) {
268
+ return 1
269
+ }
270
+
271
+ return 0
272
+ })
273
+
274
+ for (const model of models) {
275
+ add(provider.slug, provider.name, model)
276
+ }
277
+
278
+ if (options.length >= 8) {
279
+ break
280
+ }
281
+ }
282
+
283
+ return options.slice(0, 8)
284
+ }
285
+
286
+ export function toRuntimeMessage(message: ChatMessage): ThreadMessage {
287
+ const role =
288
+ message.role === 'user' || message.role === 'assistant' || message.role === 'system' ? message.role : 'assistant'
289
+
290
+ const createdAt = message.timestamp
291
+ ? new Date(message.timestamp * 1000)
292
+ : new Date(Number(message.id.match(/\d+/)?.[0]) || Date.now())
293
+
294
+ if (role === 'user') {
295
+ return {
296
+ id: message.id,
297
+ role,
298
+ content: message.parts.filter((part): part is Extract<ChatMessagePart, { type: 'text' }> => part.type === 'text'),
299
+ attachments: [],
300
+ createdAt,
301
+ metadata: { custom: { attachmentRefs: message.attachmentRefs ?? [] } }
302
+ } as ThreadMessage
303
+ }
304
+
305
+ if (role === 'system') {
306
+ const text = chatMessageText(message)
307
+
308
+ return {
309
+ id: message.id,
310
+ role,
311
+ content: [textPart(text)],
312
+ createdAt,
313
+ metadata: { custom: {} }
314
+ } as ThreadMessage
315
+ }
316
+
317
+ return {
318
+ id: message.id,
319
+ role,
320
+ content: message.parts as Extract<ThreadMessage, { role: 'assistant' }>['content'],
321
+ createdAt,
322
+ status: message.error
323
+ ? { type: 'incomplete', reason: 'error', error: message.error }
324
+ : message.pending
325
+ ? { type: 'running' }
326
+ : { type: 'complete', reason: 'stop' },
327
+ metadata: {
328
+ unstable_state: null,
329
+ unstable_annotations: [],
330
+ unstable_data: [],
331
+ steps: [],
332
+ custom: {}
333
+ }
334
+ } as ThreadMessage
335
+ }
@@ -0,0 +1,28 @@
1
+ // Routes `navigator.clipboard.writeText` through Electron IPC, since the
2
+ // renderer's clipboard API throws "Write permission denied" whenever the
3
+ // document loses focus (e.g. clicking a portaled Radix dropdown). The IPC
4
+ // path runs in the main process and is unconditional.
5
+
6
+ export function installClipboardShim() {
7
+ const ipc = window.NASTECHDesktop?.writeClipboard
8
+
9
+ if (!ipc || !navigator.clipboard) {
10
+ return
11
+ }
12
+
13
+ const native = navigator.clipboard.writeText?.bind(navigator.clipboard)
14
+
15
+ const writeText = async (text: string) => {
16
+ try {
17
+ await ipc(text)
18
+ } catch {
19
+ await native?.(text)
20
+ }
21
+ }
22
+
23
+ try {
24
+ Object.defineProperty(navigator.clipboard, 'writeText', { configurable: true, value: writeText, writable: true })
25
+ } catch {
26
+ // Browser refused override; primitives keep using the native API.
27
+ }
28
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildCommitChangelog, parseCommitHeader } from './commit-changelog'
4
+
5
+ describe('parseCommitHeader', () => {
6
+ it('extracts type, scope, and subject from a conventional header', () => {
7
+ expect(parseCommitHeader('feat(desktop): NSIS prereq detection page')).toEqual({
8
+ breaking: false,
9
+ scope: 'desktop',
10
+ subject: 'NSIS prereq detection page',
11
+ type: 'feat'
12
+ })
13
+ })
14
+
15
+ it('flags breaking changes via the `!` marker', () => {
16
+ expect(parseCommitHeader('feat(api)!: change endpoint shape')).toMatchObject({
17
+ breaking: true,
18
+ type: 'feat'
19
+ })
20
+ })
21
+
22
+ it('treats non-conventional commits as untyped with the full header as subject', () => {
23
+ expect(parseCommitHeader('Update README')).toEqual({
24
+ breaking: false,
25
+ scope: null,
26
+ subject: 'Update README',
27
+ type: null
28
+ })
29
+ })
30
+
31
+ it('ignores body lines and trims whitespace', () => {
32
+ expect(parseCommitHeader(' fix: handle null input \n\nMore detail')).toMatchObject({
33
+ subject: 'handle null input',
34
+ type: 'fix'
35
+ })
36
+ })
37
+
38
+ it('returns empty subject for blank input', () => {
39
+ expect(parseCommitHeader('')).toEqual({ breaking: false, scope: null, subject: '', type: null })
40
+ })
41
+ })
42
+
43
+ describe('buildCommitChangelog', () => {
44
+ it('groups commits into user-friendly buckets and capitalizes subjects', () => {
45
+ const groups = buildCommitChangelog([
46
+ { summary: 'feat(desktop): add NSIS prereq detection page' },
47
+ { summary: 'fix(sidebar): jitter when dragging' },
48
+ { summary: 'perf: shave 200ms off cold start' },
49
+ { summary: 'refactor: extract sidebar row component' }
50
+ ])
51
+
52
+ expect(groups.map(g => g.id)).toEqual(['new', 'fixed', 'faster'])
53
+ expect(groups[0]).toMatchObject({ label: "What's new" })
54
+ expect(groups[0].items[0]).toBe('Add NSIS prereq detection page')
55
+ expect(groups[1].items[0]).toBe('Jitter when dragging')
56
+ })
57
+
58
+ it('hides chore/ci/docs/test commits', () => {
59
+ const groups = buildCommitChangelog([
60
+ { summary: 'chore: bump deps' },
61
+ { summary: 'ci: tweak workflow' },
62
+ { summary: 'docs: spelling fix' },
63
+ { summary: 'feat: real new feature' }
64
+ ])
65
+
66
+ expect(groups).toHaveLength(1)
67
+ expect(groups[0].items).toEqual(['Real new feature'])
68
+ })
69
+
70
+ it('routes unparseable commits to the "Other improvements" bucket', () => {
71
+ const groups = buildCommitChangelog([{ summary: 'Update sidebar styling' }])
72
+
73
+ expect(groups[0].id).toBe('other')
74
+ expect(groups[0].items).toEqual(['Update sidebar styling'])
75
+ })
76
+
77
+ it('falls back to a neutral placeholder when every commit is filtered or empty', () => {
78
+ const groups = buildCommitChangelog([{ summary: 'chore: bump' }, { summary: 'ci: stuff' }])
79
+
80
+ expect(groups).toEqual([{ id: 'other', items: ['Improvements and fixes'], label: 'In this update' }])
81
+ })
82
+
83
+ it('dedupes identical subjects and caps the items per group', () => {
84
+ const groups = buildCommitChangelog(
85
+ [
86
+ { summary: 'fix: thing A' },
87
+ { summary: 'fix: thing A' },
88
+ { summary: 'fix: thing B' },
89
+ { summary: 'fix: thing C' },
90
+ { summary: 'fix: thing D' },
91
+ { summary: 'fix: thing E' }
92
+ ],
93
+ { maxPerGroup: 3, maxTotal: 10 }
94
+ )
95
+
96
+ expect(groups[0].items).toEqual(['Thing A', 'Thing B', 'Thing C'])
97
+ })
98
+
99
+ it('caps total entries across buckets', () => {
100
+ const groups = buildCommitChangelog(
101
+ [
102
+ { summary: 'feat: a' },
103
+ { summary: 'feat: b' },
104
+ { summary: 'fix: c' },
105
+ { summary: 'fix: d' },
106
+ { summary: 'perf: e' }
107
+ ],
108
+ { maxTotal: 3 }
109
+ )
110
+
111
+ const totalItems = groups.reduce((sum, g) => sum + g.items.length, 0)
112
+ expect(totalItems).toBe(3)
113
+ })
114
+ })
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tiny user-facing changelog builder. Takes a list of raw commit summaries,
3
+ * parses the Conventional Commits 1.0 header (`type(scope)!: subject`),
4
+ * filters internal noise (chore/ci/docs/...), and groups the rest into
5
+ * friendly buckets for end users (What's new, Fixed, Faster, Improved).
6
+ *
7
+ * Inlined (rather than depending on `conventional-commits-parser`) because
8
+ * that package's index re-exports a Node `stream` helper which won't load
9
+ * in the sandboxed Electron renderer, and its actual parse logic for the
10
+ * header is a small regex.
11
+ */
12
+
13
+ export type CommitGroupId = 'new' | 'fixed' | 'faster' | 'improved' | 'other'
14
+
15
+ export interface CommitGroup {
16
+ id: CommitGroupId
17
+ label: string
18
+ items: string[]
19
+ }
20
+
21
+ export interface ParsedCommit {
22
+ type: null | string
23
+ scope: null | string
24
+ breaking: boolean
25
+ subject: string
26
+ }
27
+
28
+ export interface CommitChangelogInput {
29
+ summary?: string
30
+ }
31
+
32
+ interface BuildOptions {
33
+ maxGroups?: number
34
+ maxPerGroup?: number
35
+ maxTotal?: number
36
+ }
37
+
38
+ const GROUP_META: Record<CommitGroupId, { label: string; order: number }> = {
39
+ new: { label: "What's new", order: 0 },
40
+ fixed: { label: 'Fixed', order: 1 },
41
+ faster: { label: 'Faster', order: 2 },
42
+ improved: { label: 'Improved', order: 3 },
43
+ other: { label: 'Other improvements', order: 4 }
44
+ }
45
+
46
+ const TYPE_TO_GROUP: Record<string, CommitGroupId> = {
47
+ feat: 'new',
48
+ feature: 'new',
49
+ fix: 'fixed',
50
+ bugfix: 'fixed',
51
+ hotfix: 'fixed',
52
+ revert: 'fixed',
53
+ perf: 'faster',
54
+ performance: 'faster',
55
+ refactor: 'improved',
56
+ a11y: 'improved',
57
+ ui: 'improved',
58
+ ux: 'improved'
59
+ }
60
+
61
+ const HIDDEN_TYPES = new Set([
62
+ 'build',
63
+ 'chore',
64
+ 'ci',
65
+ 'dep',
66
+ 'deps',
67
+ 'doc',
68
+ 'docs',
69
+ 'lint',
70
+ 'release',
71
+ 'style',
72
+ 'test',
73
+ 'tests',
74
+ 'wip'
75
+ ])
76
+
77
+ const FALLBACK_GROUP: CommitGroup = { id: 'other', items: ['Improvements and fixes'], label: 'In this update' }
78
+
79
+ const CONVENTIONAL_HEADER = /^(?<type>[a-zA-Z][a-zA-Z0-9_-]*)(?:\((?<scope>[^)]+)\))?(?<bang>!)?:\s+(?<subject>.+)$/
80
+
81
+ /** Parse a single commit header line per Conventional Commits 1.0. */
82
+ export function parseCommitHeader(raw: string): ParsedCommit {
83
+ const header = (raw ?? '').split(/\r?\n/, 1)[0].trim()
84
+
85
+ if (!header) {
86
+ return { breaking: false, scope: null, subject: '', type: null }
87
+ }
88
+
89
+ const match = CONVENTIONAL_HEADER.exec(header)
90
+
91
+ if (!match?.groups) {
92
+ return { breaking: false, scope: null, subject: header, type: null }
93
+ }
94
+
95
+ return {
96
+ breaking: Boolean(match.groups.bang),
97
+ scope: match.groups.scope ?? null,
98
+ subject: match.groups.subject.trim(),
99
+ type: match.groups.type.toLowerCase()
100
+ }
101
+ }
102
+
103
+ function tidySubject(subject: string): string {
104
+ const cleaned = subject
105
+ .replace(/\s+/g, ' ')
106
+ .replace(/[.;,\s]+$/, '')
107
+ .trim()
108
+
109
+ if (!cleaned) {
110
+ return cleaned
111
+ }
112
+
113
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1)
114
+ }
115
+
116
+ /**
117
+ * Build a small grouped changelog from a list of raw commits.
118
+ * Always returns at least one group; falls back to a neutral placeholder
119
+ * when every commit was filtered or unparseable.
120
+ */
121
+ export function buildCommitChangelog(
122
+ commits: readonly CommitChangelogInput[] | undefined,
123
+ options: BuildOptions = {}
124
+ ): CommitGroup[] {
125
+ const { maxGroups = 3, maxPerGroup = 4, maxTotal = 6 } = options
126
+ const groups = new Map<CommitGroupId, string[]>()
127
+ const seen = new Set<string>()
128
+ let total = 0
129
+
130
+ for (const commit of commits ?? []) {
131
+ if (total >= maxTotal) {
132
+ break
133
+ }
134
+
135
+ const parsed = parseCommitHeader(commit.summary ?? '')
136
+
137
+ if (parsed.type && HIDDEN_TYPES.has(parsed.type)) {
138
+ continue
139
+ }
140
+
141
+ const groupId: CommitGroupId = parsed.type ? (TYPE_TO_GROUP[parsed.type] ?? 'other') : 'other'
142
+ const subject = tidySubject(parsed.subject)
143
+
144
+ if (!subject) {
145
+ continue
146
+ }
147
+
148
+ const dedupeKey = subject.toLowerCase()
149
+
150
+ if (seen.has(dedupeKey)) {
151
+ continue
152
+ }
153
+
154
+ const bucket = groups.get(groupId) ?? []
155
+
156
+ if (bucket.length >= maxPerGroup) {
157
+ continue
158
+ }
159
+
160
+ bucket.push(subject)
161
+ groups.set(groupId, bucket)
162
+ seen.add(dedupeKey)
163
+ total += 1
164
+ }
165
+
166
+ const result = Array.from(groups.entries())
167
+ .map(([id, items]) => ({ id, items, label: GROUP_META[id].label, order: GROUP_META[id].order }))
168
+ .sort((a, b) => a.order - b.order)
169
+ .slice(0, maxGroups)
170
+ .map(({ id, items, label }): CommitGroup => ({ id, items, label }))
171
+
172
+ if (result.length === 0) {
173
+ return [FALLBACK_GROUP]
174
+ }
175
+
176
+ return result
177
+ }