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,185 @@
1
+ import type { ComponentProps, ReactNode } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
5
+ import { cn } from '@/lib/utils'
6
+
7
+ // Shared chrome styling for interactive statusbar items (button / link / menu
8
+ // trigger). The 'text' variant intentionally omits hover/transition/disabled.
9
+ const STATUSBAR_ACTION_CLASS =
10
+ 'inline-flex h-full items-center gap-1 rounded-none px-1.5 text-[0.6875rem] text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45'
11
+
12
+ export interface StatusbarMenuItem {
13
+ id: string
14
+ icon?: ReactNode
15
+ label: string
16
+ className?: string
17
+ disabled?: boolean
18
+ hidden?: boolean
19
+ href?: string
20
+ onSelect?: () => void
21
+ title?: string
22
+ to?: string
23
+ }
24
+
25
+ export interface StatusbarItem {
26
+ id: string
27
+ label?: ReactNode
28
+ detail?: ReactNode
29
+ icon?: ReactNode
30
+ className?: string
31
+ disabled?: boolean
32
+ hidden?: boolean
33
+ href?: string
34
+ menuAlign?: 'center' | 'end' | 'start'
35
+ menuClassName?: string
36
+ menuContent?: ReactNode
37
+ menuItems?: readonly StatusbarMenuItem[]
38
+ onSelect?: () => void
39
+ title?: string
40
+ to?: string
41
+ variant?: 'action' | 'link' | 'menu' | 'text'
42
+ }
43
+
44
+ export type StatusbarItemSide = 'left' | 'right'
45
+ export type SetStatusbarItemGroup = (id: string, items: readonly StatusbarItem[], side?: StatusbarItemSide) => void
46
+
47
+ interface StatusbarControlsProps extends ComponentProps<'footer'> {
48
+ leftItems?: readonly StatusbarItem[]
49
+ items?: readonly StatusbarItem[]
50
+ }
51
+
52
+ export function StatusbarControls({ className, leftItems = [], items = [], ...props }: StatusbarControlsProps) {
53
+ const navigate = useNavigate()
54
+
55
+ return (
56
+ <footer
57
+ className={cn(
58
+ 'flex h-5 shrink-0 items-stretch justify-between gap-2 border-t border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background) px-1 py-0 text-(--ui-text-tertiary) [-webkit-app-region:no-drag]',
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ {/* `overflow-x-clip` (not `overflow-x-auto`) so a wide status item — for
64
+ example "Connecting…" on a fresh/untitled session — can't paint a
65
+ horizontal scrollbar across the bottom of the window. Items already
66
+ `truncate` their labels, so clipping is the right behavior. */}
67
+ <div className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip">
68
+ {leftItems
69
+ .filter(item => !item.hidden)
70
+ .map(item => (
71
+ <StatusbarItemView item={item} key={`left:${item.id}`} navigate={navigate} />
72
+ ))}
73
+ </div>
74
+ <div className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip">
75
+ {items
76
+ .filter(item => !item.hidden)
77
+ .map(item => (
78
+ <StatusbarItemView item={item} key={`right:${item.id}`} navigate={navigate} />
79
+ ))}
80
+ </div>
81
+ </footer>
82
+ )
83
+ }
84
+
85
+ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: ReturnType<typeof useNavigate> }) {
86
+ const content = (
87
+ <>
88
+ {item.icon}
89
+ {item.label && <span className="truncate">{item.label}</span>}
90
+ {item.detail && <span className="truncate text-muted-foreground/80">{item.detail}</span>}
91
+ </>
92
+ )
93
+
94
+ if (item.variant === 'menu' && (item.menuContent || (item.menuItems && item.menuItems.length > 0))) {
95
+ return (
96
+ <DropdownMenu>
97
+ <DropdownMenuTrigger asChild>
98
+ <button className={cn(STATUSBAR_ACTION_CLASS, item.className)} disabled={item.disabled} type="button">
99
+ {content}
100
+ </button>
101
+ </DropdownMenuTrigger>
102
+ <DropdownMenuContent
103
+ align={item.menuAlign ?? 'start'}
104
+ className={cn('w-56', item.menuContent && 'p-0', item.menuClassName)}
105
+ side="top"
106
+ sideOffset={8}
107
+ >
108
+ {item.menuContent
109
+ ? item.menuContent
110
+ : (item.menuItems ?? [])
111
+ .filter(menuItem => !menuItem.hidden)
112
+ .map(menuItem => (
113
+ <DropdownMenuItem
114
+ className={cn('gap-2 text-foreground focus:bg-accent [&_svg]:size-4', menuItem.className)}
115
+ disabled={menuItem.disabled}
116
+ key={menuItem.id}
117
+ onSelect={() => {
118
+ if (menuItem.to) {
119
+ navigate(menuItem.to)
120
+ }
121
+
122
+ menuItem.onSelect?.()
123
+ }}
124
+ >
125
+ {menuItem.href ? (
126
+ <a
127
+ className="inline-flex w-full items-center gap-2"
128
+ href={menuItem.href}
129
+ rel="noreferrer"
130
+ target="_blank"
131
+ >
132
+ {menuItem.icon}
133
+ <span className="truncate">{menuItem.label}</span>
134
+ </a>
135
+ ) : (
136
+ <>
137
+ {menuItem.icon}
138
+ <span className="truncate">{menuItem.label}</span>
139
+ </>
140
+ )}
141
+ </DropdownMenuItem>
142
+ ))}
143
+ </DropdownMenuContent>
144
+ </DropdownMenu>
145
+ )
146
+ }
147
+
148
+ if (item.variant === 'text' && !item.onSelect && !item.to && !item.href) {
149
+ return (
150
+ <div
151
+ className={cn(
152
+ 'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] text-(--ui-text-tertiary)',
153
+ item.className
154
+ )}
155
+ >
156
+ {content}
157
+ </div>
158
+ )
159
+ }
160
+
161
+ if (item.href || item.variant === 'link') {
162
+ return (
163
+ <a className={cn(STATUSBAR_ACTION_CLASS, item.className)} href={item.href} rel="noreferrer" target="_blank">
164
+ {content}
165
+ </a>
166
+ )
167
+ }
168
+
169
+ return (
170
+ <button
171
+ className={cn(STATUSBAR_ACTION_CLASS, item.className)}
172
+ disabled={item.disabled}
173
+ onClick={() => {
174
+ if (item.to) {
175
+ navigate(item.to)
176
+ }
177
+
178
+ item.onSelect?.()
179
+ }}
180
+ type="button"
181
+ >
182
+ {content}
183
+ </button>
184
+ )
185
+ }
@@ -0,0 +1,244 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import type { ComponentProps, ReactNode } from 'react'
3
+ import { useLocation, useNavigate } from 'react-router-dom'
4
+
5
+ import { Button } from '@/components/ui/button'
6
+ import { Codicon } from '@/components/ui/codicon'
7
+ import { useI18n } from '@/i18n'
8
+ import { triggerHaptic } from '@/lib/haptics'
9
+ import { cn } from '@/lib/utils'
10
+ import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
11
+ import { toggleKeybindPanel } from '@/store/keybinds'
12
+ import {
13
+ $fileBrowserOpen,
14
+ $panesFlipped,
15
+ $sidebarOpen,
16
+ toggleFileBrowserOpen,
17
+ togglePanesFlipped,
18
+ toggleSidebarOpen
19
+ } from '@/store/layout'
20
+
21
+ import { appViewForPath, isOverlayView } from '../routes'
22
+
23
+ import { titlebarButtonClass } from './titlebar'
24
+
25
+ export interface TitlebarTool {
26
+ id: string
27
+ label: string
28
+ active?: boolean
29
+ className?: string
30
+ disabled?: boolean
31
+ hidden?: boolean
32
+ href?: string
33
+ icon: ReactNode
34
+ onSelect?: () => void
35
+ title?: string
36
+ to?: string
37
+ }
38
+
39
+ export type TitlebarToolSide = 'left' | 'right'
40
+ export type SetTitlebarToolGroup = (id: string, tools: readonly TitlebarTool[], side?: TitlebarToolSide) => void
41
+
42
+ interface TitlebarControlsProps extends ComponentProps<'div'> {
43
+ leftTools?: readonly TitlebarTool[]
44
+ tools?: readonly TitlebarTool[]
45
+ onOpenSettings: () => void
46
+ }
47
+
48
+ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: TitlebarControlsProps) {
49
+ const { t } = useI18n()
50
+ const navigate = useNavigate()
51
+ const location = useLocation()
52
+ const hapticsMuted = useStore($hapticsMuted)
53
+ const fileBrowserOpen = useStore($fileBrowserOpen)
54
+ const sidebarOpen = useStore($sidebarOpen)
55
+ const panesFlipped = useStore($panesFlipped)
56
+
57
+ const toggleHaptics = () => {
58
+ if (!hapticsMuted) {
59
+ triggerHaptic('tap')
60
+ }
61
+
62
+ toggleHapticsMuted()
63
+
64
+ if (hapticsMuted) {
65
+ window.requestAnimationFrame(() => triggerHaptic('success'))
66
+ }
67
+ }
68
+
69
+ // Each titlebar button controls the pane physically on its side, so a flip
70
+ // swaps which pane each one toggles. Default: sessions left, file browser
71
+ // right. Flipped: file browser left, sessions right. Sidebar toggles never
72
+ // carry an active highlight — they're plain show/hide affordances.
73
+ const fileBrowserEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
74
+ const sessionsEdge = { open: sidebarOpen, toggle: toggleSidebarOpen }
75
+ const leftEdge = panesFlipped ? fileBrowserEdge : sessionsEdge
76
+ const rightEdge = panesFlipped ? sessionsEdge : fileBrowserEdge
77
+
78
+ const leftToolbarTools: TitlebarTool[] = [
79
+ {
80
+ icon: <Codicon name="layout-sidebar-left" />,
81
+ id: 'sidebar',
82
+ label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar,
83
+ onSelect: () => {
84
+ triggerHaptic('tap')
85
+ leftEdge.toggle()
86
+ }
87
+ },
88
+ {
89
+ icon: <Codicon name="arrow-swap" />,
90
+ id: 'flip-panes',
91
+ label: t.titlebar.swapSidebarSides,
92
+ onSelect: () => {
93
+ triggerHaptic('tap')
94
+ togglePanesFlipped()
95
+ },
96
+ title: t.titlebar.swapSidebarSidesTitle
97
+ },
98
+ ...leftTools
99
+ ]
100
+
101
+ const rightSidebarTool: TitlebarTool = {
102
+ icon: <Codicon name="layout-sidebar-right" />,
103
+ id: 'right-sidebar',
104
+ label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar,
105
+ onSelect: () => {
106
+ triggerHaptic('tap')
107
+ rightEdge.toggle()
108
+ }
109
+ }
110
+
111
+ // Static system tools — always pinned to the screen's right edge.
112
+ const systemTools: TitlebarTool[] = [
113
+ {
114
+ active: hapticsMuted,
115
+ icon: <Codicon name={hapticsMuted ? 'mute' : 'unmute'} />,
116
+ id: 'haptics',
117
+ label: hapticsMuted ? t.titlebar.unmuteHaptics : t.titlebar.muteHaptics,
118
+ onSelect: toggleHaptics
119
+ },
120
+ {
121
+ icon: <Codicon name="keyboard" />,
122
+ id: 'keybinds',
123
+ label: t.titlebar.openKeybinds,
124
+ onSelect: () => {
125
+ triggerHaptic('open')
126
+ toggleKeybindPanel()
127
+ }
128
+ },
129
+ {
130
+ icon: <Codicon name="settings-gear" />,
131
+ id: 'settings',
132
+ label: t.titlebar.openSettings,
133
+ onSelect: () => {
134
+ triggerHaptic('open')
135
+ onOpenSettings()
136
+ }
137
+ }
138
+ ]
139
+
140
+ // While a full-screen overlay (settings, command center, …) is open it should
141
+ // visually own the window. These control clusters are `fixed` at a higher
142
+ // z-index than the overlay card, so they'd otherwise bleed over it — hide them
143
+ // and let the overlay's own chrome (close button, drag region) take over.
144
+ if (isOverlayView(appViewForPath(location.pathname))) {
145
+ return null
146
+ }
147
+
148
+ const visibleSystemTools = systemTools.filter(tool => !tool.hidden)
149
+ const settingsTool = visibleSystemTools.find(tool => tool.id === 'settings')
150
+ const visibleSystemToolsBeforeSettings = visibleSystemTools.filter(tool => tool.id !== 'settings')
151
+ const visiblePaneTools = tools.filter(tool => !tool.hidden)
152
+
153
+ return (
154
+ <>
155
+ <div
156
+ aria-label={t.shell.windowControls}
157
+ className="fixed left-(--titlebar-controls-left) top-(--titlebar-controls-top) z-70 flex translate-y-0.5 flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
158
+ >
159
+ {leftToolbarTools
160
+ .filter(tool => !tool.hidden)
161
+ .map(tool => (
162
+ <TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
163
+ ))}
164
+ </div>
165
+
166
+ {/*
167
+ Pane-scoped tools (preview's monitor / devtools / refresh / X) render
168
+ as their own fixed cluster. AppShell sets --shell-preview-toolbar-gap
169
+ to either the static cluster's width (file-browser closed → cluster
170
+ sits flush against system tools) or the file-browser pane's width
171
+ (file-browser open → cluster sits flush against the file-browser pane,
172
+ i.e. at the preview pane's right edge). No margin hacks needed.
173
+ */}
174
+ {visiblePaneTools.length > 0 && (
175
+ <div
176
+ aria-label={t.shell.paneControls}
177
+ className="fixed top-(--titlebar-controls-top) right-[calc(var(--titlebar-tools-right)+var(--shell-preview-toolbar-gap,0))] z-70 flex flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
178
+ >
179
+ {visiblePaneTools.map(tool => (
180
+ <TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
181
+ ))}
182
+ </div>
183
+ )}
184
+
185
+ <div
186
+ aria-label={t.shell.appControls}
187
+ className="fixed right-(--titlebar-tools-right) top-(--titlebar-controls-top) z-70 flex flex-row items-center justify-end gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
188
+ >
189
+ {visibleSystemToolsBeforeSettings.map(tool => (
190
+ <TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
191
+ ))}
192
+ {settingsTool && <TitlebarToolButton navigate={navigate} tool={settingsTool} />}
193
+ <TitlebarToolButton navigate={navigate} tool={rightSidebarTool} />
194
+ </div>
195
+ </>
196
+ )
197
+ }
198
+
199
+ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof useNavigate>; tool: TitlebarTool }) {
200
+ // Titlebar actions never show an active background — state reads from the
201
+ // icon itself (e.g. the mute/unmute glyph). aria-pressed still carries it
202
+ // for a11y.
203
+ const className = cn(titlebarButtonClass, 'bg-transparent select-none', tool.className)
204
+
205
+ if (tool.href) {
206
+ return (
207
+ <Button asChild className={className} size="icon-titlebar" variant="ghost">
208
+ <a
209
+ aria-label={tool.label}
210
+ href={tool.href}
211
+ onPointerDown={event => event.stopPropagation()}
212
+ rel="noreferrer"
213
+ target="_blank"
214
+ title={tool.title ?? tool.label}
215
+ >
216
+ {tool.icon}
217
+ </a>
218
+ </Button>
219
+ )
220
+ }
221
+
222
+ return (
223
+ <Button
224
+ aria-label={tool.label}
225
+ aria-pressed={tool.active ?? undefined}
226
+ className={className}
227
+ disabled={tool.disabled}
228
+ onClick={() => {
229
+ if (tool.to) {
230
+ navigate(tool.to)
231
+ }
232
+
233
+ tool.onSelect?.()
234
+ }}
235
+ onPointerDown={event => event.stopPropagation()}
236
+ size="icon-titlebar"
237
+ title={tool.title ?? tool.label}
238
+ type="button"
239
+ variant="ghost"
240
+ >
241
+ {tool.icon}
242
+ </Button>
243
+ )
244
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ TITLEBAR_CONTROL_OFFSET_X,
5
+ TITLEBAR_EDGE_INSET,
6
+ TITLEBAR_FALLBACK_WINDOW_BUTTON_X,
7
+ titlebarControlsPosition
8
+ } from './titlebar'
9
+
10
+ describe('titlebarControlsPosition', () => {
11
+ it('offsets controls from visible traffic lights', () => {
12
+ expect(titlebarControlsPosition({ x: 24, y: 10 }).left).toBe(24 + TITLEBAR_CONTROL_OFFSET_X)
13
+ })
14
+
15
+ it('pins to the edge when macOS fullscreen hides traffic lights', () => {
16
+ expect(titlebarControlsPosition({ x: 24, y: 10 }, true).left).toBe(TITLEBAR_EDGE_INSET)
17
+ })
18
+
19
+ it('pins to the edge on Windows/Linux where native controls render on the right', () => {
20
+ expect(titlebarControlsPosition(null).left).toBe(TITLEBAR_EDGE_INSET)
21
+ })
22
+
23
+ it('uses the macOS fallback while the initial window state is unknown', () => {
24
+ expect(titlebarControlsPosition(undefined).left).toBe(TITLEBAR_FALLBACK_WINDOW_BUTTON_X + TITLEBAR_CONTROL_OFFSET_X)
25
+ })
26
+ })
@@ -0,0 +1,45 @@
1
+ import type { NasTechConnection } from '@/global'
2
+
3
+ export const TITLEBAR_HEIGHT = 34
4
+ export const MACOS_TRAFFIC_LIGHTS_HEIGHT = 14
5
+ export const TITLEBAR_ICON_SIZE = 12
6
+ export const TITLEBAR_CONTROL_OFFSET_X = 74
7
+ export const TITLEBAR_CONTROL_HEIGHT = 22
8
+ export const TITLEBAR_CONTROLS_TOP = (TITLEBAR_HEIGHT - TITLEBAR_CONTROL_HEIGHT) / 2
9
+ export const TITLEBAR_FALLBACK_WINDOW_BUTTON_X = 24
10
+ // Edge inset used when no left-side native controls take up that space —
11
+ // Windows/Linux (native overlay is on the right) and macOS fullscreen
12
+ // (traffic lights are hidden). Matches the right-cluster's 0.75rem padding.
13
+ export const TITLEBAR_EDGE_INSET = 14
14
+
15
+ // Titlebar palette only. All sizing/radius/cursor/centering come from the
16
+ // shared <Button size="icon-titlebar"> (used polymorphically via asChild) —
17
+ // Button is the single source of button styling.
18
+ export const titlebarButtonClass =
19
+ 'text-muted-foreground/85 hover:bg-(--ui-control-hover-background) hover:text-foreground'
20
+
21
+ export const titlebarHeaderBaseClass =
22
+ 'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 border-b border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-[max(0.75rem,var(--titlebar-content-inset,0rem))]'
23
+
24
+ export const titlebarHeaderShadowClass =
25
+ "after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-4 after:bg-linear-to-b after:from-(--ui-chat-surface-background) after:to-transparent after:content-['']"
26
+
27
+ export function titlebarControlsPosition(
28
+ windowButtonPosition: NasTechConnection['windowButtonPosition'] | undefined,
29
+ isFullscreen = false
30
+ ) {
31
+ const top = Math.max(0, TITLEBAR_CONTROLS_TOP)
32
+
33
+ // No left-side native controls to dodge:
34
+ // - Windows/Linux: native min/max/close render on the right via titleBarOverlay.
35
+ // - macOS fullscreen: traffic lights are hidden.
36
+ // In both cases, pin the cluster to the edge with a small inset.
37
+ if (windowButtonPosition === null || isFullscreen) {
38
+ return { left: TITLEBAR_EDGE_INSET, top }
39
+ }
40
+
41
+ return {
42
+ left: (windowButtonPosition?.x ?? TITLEBAR_FALLBACK_WINDOW_BUTTON_X) + TITLEBAR_CONTROL_OFFSET_X,
43
+ top
44
+ }
45
+ }
@@ -0,0 +1,39 @@
1
+ import { useCallback, useMemo, useState } from 'react'
2
+
3
+ type Side = 'left' | 'right'
4
+ type Groups<T> = Record<Side, Record<string, readonly T[]>>
5
+
6
+ export type GroupSetter<T> = (id: string, items: readonly T[], side?: Side) => void
7
+
8
+ interface GroupRegistry<T> {
9
+ flat: { left: T[]; right: T[] }
10
+ set: GroupSetter<T>
11
+ }
12
+
13
+ export function useGroupRegistry<T>(): GroupRegistry<T> {
14
+ const [groups, setGroups] = useState<Groups<T>>({ left: {}, right: {} })
15
+
16
+ const set = useCallback<GroupSetter<T>>((id, items, side = 'right') => {
17
+ setGroups(current => {
18
+ const next = { ...current, [side]: { ...current[side] } }
19
+
20
+ if (items.length === 0) {
21
+ delete next[side][id]
22
+ } else {
23
+ next[side][id] = items
24
+ }
25
+
26
+ return next
27
+ })
28
+ }, [])
29
+
30
+ const flat = useMemo(
31
+ () => ({
32
+ left: Object.values(groups.left).flat(),
33
+ right: Object.values(groups.right).flat()
34
+ }),
35
+ [groups]
36
+ )
37
+
38
+ return { flat, set }
39
+ }
@@ -0,0 +1,103 @@
1
+ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
2
+ import { MemoryRouter } from 'react-router-dom'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ const getSkills = vi.fn()
6
+ const getToolsets = vi.fn()
7
+ const toggleSkill = vi.fn()
8
+ const toggleToolset = vi.fn()
9
+ const getToolsetConfig = vi.fn()
10
+ const selectToolsetProvider = vi.fn()
11
+
12
+ vi.mock('@/nastech', () => ({
13
+ getSkills: () => getSkills(),
14
+ getToolsets: () => getToolsets(),
15
+ toggleSkill: (name: string, enabled: boolean) => toggleSkill(name, enabled),
16
+ toggleToolset: (name: string, enabled: boolean) => toggleToolset(name, enabled),
17
+ getToolsetConfig: (name: string) => getToolsetConfig(name),
18
+ selectToolsetProvider: (toolset: string, provider: string) => selectToolsetProvider(toolset, provider),
19
+ deleteEnvVar: vi.fn(),
20
+ revealEnvVar: vi.fn(),
21
+ setEnvVar: vi.fn()
22
+ }))
23
+
24
+ // Notifications hit nanostores/timers we don't care about here.
25
+ vi.mock('@/store/notifications', () => ({
26
+ notify: vi.fn(),
27
+ notifyError: vi.fn()
28
+ }))
29
+
30
+ function toolset(overrides: Record<string, unknown> = {}) {
31
+ return {
32
+ name: 'web',
33
+ label: 'Web Search',
34
+ description: 'web_search, web_extract',
35
+ enabled: true,
36
+ available: true,
37
+ configured: true,
38
+ tools: ['web_search', 'web_extract'],
39
+ ...overrides
40
+ }
41
+ }
42
+
43
+ function renderSkills() {
44
+ return import('./index').then(({ SkillsView }) =>
45
+ render(
46
+ <MemoryRouter initialEntries={['/skills?tab=toolsets']}>
47
+ <SkillsView />
48
+ </MemoryRouter>
49
+ )
50
+ )
51
+ }
52
+
53
+ beforeEach(() => {
54
+ getSkills.mockResolvedValue([])
55
+ getToolsets.mockResolvedValue([toolset()])
56
+ toggleToolset.mockResolvedValue({ ok: true, name: 'web', enabled: false })
57
+ getToolsetConfig.mockResolvedValue({ has_category: false, active_provider: null, providers: [] })
58
+ })
59
+
60
+ afterEach(() => {
61
+ cleanup()
62
+ vi.clearAllMocks()
63
+ })
64
+
65
+ describe('SkillsView toolset management', () => {
66
+ it('renders a switch for each toolset and toggles it off', async () => {
67
+ await renderSkills()
68
+
69
+ const sw = await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
70
+ expect(sw.getAttribute('aria-checked')).toBe('true')
71
+
72
+ fireEvent.click(sw)
73
+
74
+ await waitFor(() => expect(toggleToolset).toHaveBeenCalledWith('web', false))
75
+ })
76
+
77
+ it('renders toolset titles without leading emoji', async () => {
78
+ getToolsets.mockResolvedValue([
79
+ toolset({ name: 'cronjob', label: '⏰ Cron Jobs', description: 'cron tools' })
80
+ ])
81
+
82
+ await renderSkills()
83
+
84
+ expect(await screen.findByText('Cron Jobs')).toBeTruthy()
85
+ expect(screen.queryByText(/⏰/)).toBeNull()
86
+ })
87
+
88
+ it('keeps the configured pill alongside the switch', async () => {
89
+ await renderSkills()
90
+
91
+ await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
92
+ expect(screen.getByText('Configured')).toBeTruthy()
93
+ })
94
+
95
+ it('expands the provider config panel when the configured pill is clicked', async () => {
96
+ await renderSkills()
97
+
98
+ const configureBtn = await screen.findByRole('button', { name: 'Configure Web Search' })
99
+ fireEvent.click(configureBtn)
100
+
101
+ await waitFor(() => expect(getToolsetConfig).toHaveBeenCalledWith('web'))
102
+ })
103
+ })