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,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { extractPreviewTargets, previewTargetFromMarkdownHref, stripPreviewTargets } from './preview-targets'
4
+
5
+ describe('preview target detection', () => {
6
+ it('does not infer preview targets from raw paths or URLs', () => {
7
+ expect(extractPreviewTargets('Preview: http://localhost:5173/')).toEqual([])
8
+ expect(extractPreviewTargets('Open index.html\n/tmp/demo.html\nhttp://localhost:5173/')).toEqual([])
9
+ })
10
+
11
+ it('decodes preview markdown hrefs', () => {
12
+ expect(previewTargetFromMarkdownHref('#preview/%2Ftmp%2Fdemo.html')).toBe('/tmp/demo.html')
13
+ expect(previewTargetFromMarkdownHref('#preview:%2Ftmp%2Fdemo.html')).toBe('/tmp/demo.html')
14
+ expect(previewTargetFromMarkdownHref('#media:%2Ftmp%2Fdemo.mp4')).toBeNull()
15
+ })
16
+
17
+ it('extracts preview targets from already-rendered preview markers', () => {
18
+ expect(extractPreviewTargets('[Preview: demo.html](#preview:%2Ftmp%2Fdemo.html)')).toEqual(['/tmp/demo.html'])
19
+ })
20
+
21
+ it('strips preview targets from visible assistant text', () => {
22
+ expect(stripPreviewTargets('ready\n/tmp/mycelium-bunnies.html\nopen it')).toBe(
23
+ 'ready\n/tmp/mycelium-bunnies.html\nopen it'
24
+ )
25
+ expect(stripPreviewTargets('[Preview: demo.html](#preview:%2Ftmp%2Fdemo.html)\nopen it')).toBe('open it')
26
+ })
27
+ })
@@ -0,0 +1,63 @@
1
+ const PREVIEW_MARKDOWN_RE = /\[Preview:[^\]]+\]\((?<href>#preview[:/][^)]+)\)/gi
2
+
3
+ export function stripPreviewTargets(text: string): string {
4
+ return text
5
+ .replace(PREVIEW_MARKDOWN_RE, '')
6
+ .replace(/[ \t]+\n/g, '\n')
7
+ .replace(/\n{3,}/g, '\n\n')
8
+ .trim()
9
+ }
10
+
11
+ export function extractPreviewTargets(text: string): string[] {
12
+ const targets: string[] = []
13
+ const seen = new Set<string>()
14
+
15
+ for (const match of text.matchAll(PREVIEW_MARKDOWN_RE)) {
16
+ const target = previewTargetFromMarkdownHref(match.groups?.href)
17
+
18
+ if (target && !seen.has(target)) {
19
+ seen.add(target)
20
+ targets.push(target)
21
+ }
22
+ }
23
+
24
+ return targets
25
+ }
26
+
27
+ export function previewMarkdownHref(target: string): string {
28
+ return `#preview/${encodeURIComponent(target)}`
29
+ }
30
+
31
+ export function previewTargetFromMarkdownHref(href?: string): string | null {
32
+ if (!href?.startsWith('#preview:') && !href?.startsWith('#preview/')) {
33
+ return null
34
+ }
35
+
36
+ try {
37
+ return decodeURIComponent(href.slice('#preview'.length + 1))
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ export function previewName(target: string): string {
44
+ try {
45
+ const url = new URL(target)
46
+
47
+ if (url.protocol === 'file:') {
48
+ return decodeURIComponent(url.pathname).split(/[\\/]/).filter(Boolean).pop() || target
49
+ }
50
+
51
+ const file = url.pathname.split('/').filter(Boolean).pop()
52
+
53
+ return file || url.host
54
+ } catch {
55
+ return target.split(/[\\/]/).filter(Boolean).pop() || target
56
+ }
57
+ }
58
+
59
+ export function previewDisplayLabel(target: string): string {
60
+ const escaped = previewName(target).replace(/[[\]\\]/g, '\\$&')
61
+
62
+ return `Preview: ${escaped}`
63
+ }
@@ -0,0 +1,58 @@
1
+ // Deterministic per-profile color so a profile is glanceable across the app
2
+ // (the sidebar profile rail). The default/root profile has no color — named
3
+ // profiles get a stable hue derived from the name, so the same profile always
4
+ // reads the same color without persisting anything.
5
+
6
+ const PROFILE_TAG_SATURATION = 68
7
+ const PROFILE_TAG_LIGHTNESS = 58
8
+
9
+ function hashString(value: string): number {
10
+ let hash = 0
11
+
12
+ for (let index = 0; index < value.length; index += 1) {
13
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0
14
+ }
15
+
16
+ return hash
17
+ }
18
+
19
+ // Returns an hsl() string for a named profile, or null for default/empty
20
+ // (rendered neutral / untagged).
21
+ export function profileColor(name: null | string | undefined): null | string {
22
+ const key = (name ?? '').trim()
23
+
24
+ if (!key || key === 'default') {
25
+ return null
26
+ }
27
+
28
+ const hue = hashString(key) % 360
29
+
30
+ return `hsl(${hue} ${PROFILE_TAG_SATURATION}% ${PROFILE_TAG_LIGHTNESS}%)`
31
+ }
32
+
33
+ // A profile's effective color: a user-picked override wins, else the
34
+ // deterministic hue. Default/empty stays neutral (null) regardless.
35
+ export function resolveProfileColor(
36
+ name: null | string | undefined,
37
+ overrides: Record<string, string>
38
+ ): null | string {
39
+ const key = (name ?? '').trim()
40
+
41
+ if (!key || key === 'default') {
42
+ return null
43
+ }
44
+
45
+ return overrides[key] ?? profileColor(key)
46
+ }
47
+
48
+ // Curated swatches for the rail color picker — evenly spaced hues at the same
49
+ // saturation/lightness as the deterministic palette, so picks stay cohesive.
50
+ export const PROFILE_SWATCHES: readonly string[] = Array.from(
51
+ { length: 12 },
52
+ (_, index) => `hsl(${index * 30} ${PROFILE_TAG_SATURATION}% ${PROFILE_TAG_LIGHTNESS}%)`
53
+ )
54
+
55
+ // Translucent fill derived from a profile color, for tag backgrounds.
56
+ export function profileColorSoft(color: string, percent = 16): string {
57
+ return `color-mix(in srgb, ${color} ${percent}%, transparent)`
58
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { isProviderSetupErrorMessage } from './provider-setup-errors'
4
+
5
+ describe('isProviderSetupErrorMessage', () => {
6
+ it('matches generic missing-provider copy', () => {
7
+ expect(isProviderSetupErrorMessage('No inference provider configured. Run `NASTECH model` to choose one.')).toBe(
8
+ true
9
+ )
10
+ expect(isProviderSetupErrorMessage('No inference provider is configured.')).toBe(true)
11
+ expect(isProviderSetupErrorMessage('No NasTech provider is configured.')).toBe(true)
12
+ expect(isProviderSetupErrorMessage('set an API key (OPENROUTER_API_KEY) in ~/.NASTECH/.env')).toBe(true)
13
+ })
14
+
15
+ it('does not match non-provider runtime failures', () => {
16
+ expect(
17
+ isProviderSetupErrorMessage('Selected runtime is not available. setup.status reports configured credentials.')
18
+ ).toBe(false)
19
+ })
20
+
21
+ it('returns false for empty input', () => {
22
+ expect(isProviderSetupErrorMessage('')).toBe(false)
23
+ expect(isProviderSetupErrorMessage(null)).toBe(false)
24
+ expect(isProviderSetupErrorMessage(undefined)).toBe(false)
25
+ })
26
+ })
@@ -0,0 +1,12 @@
1
+ const PROVIDER_SETUP_ERROR_RE =
2
+ /No (?:inference|NasTech) provider(?: is)? configured|no_provider_configured|OPENROUTER_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|set an API key/i
3
+
4
+ export function isProviderSetupErrorMessage(message: null | string | undefined): boolean {
5
+ const text = message?.trim()
6
+
7
+ if (!text) {
8
+ return false
9
+ }
10
+
11
+ return PROVIDER_SETUP_ERROR_RE.test(text)
12
+ }
@@ -0,0 +1,13 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+
3
+ // Shared React Query client. Lives in its own module (not main.tsx) so non-React
4
+ // code — e.g. the profile store on a gateway swap — can invalidate cached,
5
+ // profile-scoped settings without importing the app entry point.
6
+ export const queryClient = new QueryClient({
7
+ defaultOptions: {
8
+ queries: {
9
+ refetchOnWindowFocus: false,
10
+ staleTime: 60_000
11
+ }
12
+ }
13
+ })
@@ -0,0 +1,105 @@
1
+ import { parseMarkdownIntoBlocks } from '@assistant-ui/react-streamdown'
2
+ import remend from 'remend'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { findRemendWindowStart, tailBoundedRemend } from './remend-tail'
6
+
7
+ const CORPUS = `# Heading one
8
+
9
+ Intro paragraph with **bold**, *italic*, \`inline code\`, and a [link](https://example.com).
10
+
11
+ ## Code
12
+
13
+ \`\`\`python
14
+ def main():
15
+ cost = "$5"
16
+ print(f"total: $\{cost}")
17
+ \`\`\`
18
+
19
+ Some text after the fence with $x^2 + y^2$ inline math.
20
+
21
+ $$
22
+ \\int_0^1 f(x) dx
23
+ $$
24
+
25
+ - list item one with **bold**
26
+ - list item two
27
+
28
+ | col a | col b |
29
+ | ----- | ----- |
30
+ | 1 | 2 |
31
+
32
+ ~~~js
33
+ const s = \`template \${value}\`
34
+ ~~~
35
+
36
+ Final paragraph with ~~strike~~ and unfinished [link text](https://exa
37
+ `
38
+
39
+ /**
40
+ * Render-equivalence oracle: full-text remend and tail-bounded remend may
41
+ * differ in raw string output ONLY in ways that cannot affect rendering —
42
+ * i.e. after block splitting, every block must be identical. (Streamdown
43
+ * renders blocks independently, so block-level equality IS render equality.)
44
+ */
45
+ function blocksOf(text: string): string[] {
46
+ return parseMarkdownIntoBlocks(text)
47
+ }
48
+
49
+ describe('tailBoundedRemend', () => {
50
+ it('matches full remend block output at every streaming prefix', () => {
51
+ for (let end = 1; end <= CORPUS.length; end++) {
52
+ const prefix = CORPUS.slice(0, end)
53
+ const full = blocksOf(remend(prefix))
54
+ const tail = blocksOf(tailBoundedRemend(prefix))
55
+
56
+ expect(tail, `prefix length ${end}: ${JSON.stringify(prefix.slice(-60))}`).toEqual(full)
57
+ }
58
+ })
59
+
60
+ it('repairs an unclosed fence opened early in a long message', () => {
61
+ const text = `intro\n\n\`\`\`python\n${'x = 1\n'.repeat(500)}print("$dollar")`
62
+ const repaired = tailBoundedRemend(text)
63
+
64
+ expect(blocksOf(repaired)).toEqual(blocksOf(remend(text)))
65
+ // the window must reach back to the fence opener
66
+ expect(findRemendWindowStart(text)).toBe(text.indexOf('```python'))
67
+ })
68
+
69
+ it('bounds the window to the tail paragraph when no fence is open', () => {
70
+ const text = `para one\n\npara two\n\npara three with **bold`
71
+ const start = findRemendWindowStart(text)
72
+
73
+ expect(start).toBe(text.indexOf('para three'))
74
+ expect(tailBoundedRemend(text)).toBe(remend(text))
75
+ })
76
+
77
+ it('widens the window across an open $$ math block', () => {
78
+ const text = `before\n\n$$\n\\frac{a}{b}`
79
+ const start = findRemendWindowStart(text)
80
+
81
+ expect(start).toBeLessThanOrEqual(text.indexOf('$$'))
82
+ expect(blocksOf(tailBoundedRemend(text))).toEqual(blocksOf(remend(text)))
83
+ })
84
+
85
+ it('handles closed constructs without modification', () => {
86
+ const text = `done **bold** and \`code\`\n\n\`\`\`js\nconst a = 1\n\`\`\`\n\nlast line.`
87
+
88
+ expect(tailBoundedRemend(text)).toBe(text)
89
+ })
90
+
91
+ it('intentionally diverges from full remend on cross-block dangling openers', () => {
92
+ // Full remend scans the whole document and appends `**` for an opener
93
+ // left dangling in an EARLIER block, dumping stray asterisks into the
94
+ // unrelated tail block ("|**"). Because Streamdown splits into blocks
95
+ // after the repair, that opener never renders as bold either way — the
96
+ // tail-bounded result is the cleaner of the two. This test documents
97
+ // the divergence so a future remend upgrade that changes the behavior
98
+ // gets noticed.
99
+ const text = `- item with **dangling\n- item two\n\n|`
100
+
101
+ expect(remend(text).endsWith('|**')).toBe(true)
102
+ expect(tailBoundedRemend(text).endsWith('|')).toBe(true)
103
+ expect(tailBoundedRemend(text).endsWith('|**')).toBe(false)
104
+ })
105
+ })
@@ -0,0 +1,108 @@
1
+ import remend from 'remend'
2
+
3
+ // Tail-bounded incomplete-markdown repair.
4
+ //
5
+ // Streamdown's built-in `parseIncompleteMarkdown` runs `remend` over the whole
6
+ // accumulated message on every streaming flush (~18% of script time on 50KB+
7
+ // messages). But repairs only ever matter in the trailing block: inline
8
+ // constructs can't cross a blank line, and Streamdown splits into blocks AFTER
9
+ // the repair, so a dangling opener in an earlier block can't reach the tail.
10
+ // We run `remend` on just that block instead.
11
+
12
+ const BACKTICK = 96 // `
13
+ const TILDE = 126 // ~
14
+ const SPACE = 32
15
+ const TAB = 9
16
+ const BACKSLASH = 92
17
+
18
+ const isSpace = (c: number) => c === SPACE || c === TAB
19
+
20
+ /**
21
+ * Index of the last top-level block start — the char after the most recent
22
+ * blank line that sits outside any open code fence or `$$` math block. An
23
+ * unclosed fence/math always begins after that blank, so it stays wholly
24
+ * inside the window without separate tracking. One cheap char pass, no regex.
25
+ */
26
+ export function findRemendWindowStart(text: string): number {
27
+ const n = text.length
28
+ let inFence = false
29
+ let fenceChar = 0
30
+ let fenceRun = 0
31
+ let inMath = false
32
+ let boundary = 0
33
+ let pending = -1 // a blank line, committed to `boundary` once content follows
34
+
35
+ for (let lineStart = 0; lineStart <= n; ) {
36
+ let lineEnd = text.indexOf('\n', lineStart)
37
+
38
+ if (lineEnd === -1) {
39
+ lineEnd = n
40
+ }
41
+
42
+ let i = lineStart
43
+
44
+ while (i < lineEnd && isSpace(text.charCodeAt(i))) {
45
+ i += 1
46
+ }
47
+
48
+ const first = i < lineEnd ? text.charCodeAt(i) : -1
49
+ let marker = false
50
+
51
+ // Fence open/close (``` or ~~~, ≤3 spaces indent).
52
+ if ((first === BACKTICK || first === TILDE) && i - lineStart <= 3) {
53
+ let run = i
54
+
55
+ while (run < lineEnd && text.charCodeAt(run) === first) {
56
+ run += 1
57
+ }
58
+
59
+ if (run - i >= 3) {
60
+ marker = true
61
+
62
+ if (!inFence) {
63
+ inFence = true
64
+ fenceChar = first
65
+ fenceRun = run - i
66
+ } else if (first === fenceChar && run - i >= fenceRun && onlyWhitespace(text, run, lineEnd)) {
67
+ inFence = false
68
+ }
69
+ }
70
+ }
71
+
72
+ // Toggle `$$` math state on plain lines ($$ inside a fence is literal).
73
+ if (!inFence && !marker) {
74
+ for (let s = text.indexOf('$$', lineStart); s !== -1 && s < lineEnd - 1; s = text.indexOf('$$', s + 2)) {
75
+ if (s === 0 || text.charCodeAt(s - 1) !== BACKSLASH) {
76
+ inMath = !inMath
77
+ }
78
+ }
79
+ }
80
+
81
+ if (first === -1 && !inFence && !inMath) {
82
+ pending = lineEnd + 1
83
+ } else if (pending !== -1) {
84
+ boundary = pending
85
+ pending = -1
86
+ }
87
+
88
+ lineStart = lineEnd + 1
89
+ }
90
+
91
+ return boundary
92
+ }
93
+
94
+ function onlyWhitespace(text: string, from: number, to: number): boolean {
95
+ for (let i = from; i < to; i += 1) {
96
+ if (!isSpace(text.charCodeAt(i))) {
97
+ return false
98
+ }
99
+ }
100
+
101
+ return true
102
+ }
103
+
104
+ export function tailBoundedRemend(text: string): string {
105
+ const start = findRemendWindowStart(text)
106
+
107
+ return start <= 0 ? remend(text) : text.slice(0, start) + remend(text.slice(start))
108
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { interpretRuntimeReadiness } from './runtime-readiness'
4
+
5
+ describe('interpretRuntimeReadiness', () => {
6
+ it('prefers runtime_check when both signals exist', () => {
7
+ const result = interpretRuntimeReadiness({
8
+ setup: { provider_configured: false },
9
+ setupError: null,
10
+ runtime: { ok: true },
11
+ runtimeError: null
12
+ })
13
+
14
+ expect(result).toEqual({
15
+ checksDisagree: true,
16
+ ready: true,
17
+ reason: null,
18
+ source: 'runtime_check'
19
+ })
20
+ })
21
+
22
+ it('surfaces runtime mismatch details when runtime_check fails', () => {
23
+ const result = interpretRuntimeReadiness({
24
+ setup: { provider_configured: true },
25
+ setupError: null,
26
+ runtime: { error: 'No provider can serve the selected model.', ok: false },
27
+ runtimeError: null
28
+ })
29
+
30
+ expect(result.ready).toBe(false)
31
+ expect(result.source).toBe('runtime_check')
32
+ expect(result.checksDisagree).toBe(true)
33
+ expect(result.reason).toContain('No provider can serve the selected model.')
34
+ expect(result.reason).toContain('setup.status reports configured credentials')
35
+ })
36
+
37
+ it('falls back to setup.status when runtime_check has no boolean result', () => {
38
+ const result = interpretRuntimeReadiness({
39
+ setup: { provider_configured: true },
40
+ setupError: null,
41
+ runtime: null,
42
+ runtimeError: 'runtime check RPC unavailable'
43
+ })
44
+
45
+ expect(result).toEqual({
46
+ checksDisagree: false,
47
+ ready: true,
48
+ reason: null,
49
+ source: 'setup_status'
50
+ })
51
+ })
52
+
53
+ it('uses explicit fallback when both checks are missing', () => {
54
+ const result = interpretRuntimeReadiness({
55
+ setup: null,
56
+ setupError: 'setup.status timeout',
57
+ runtime: null,
58
+ runtimeError: 'setup.runtime_check timeout'
59
+ })
60
+
61
+ expect(result.ready).toBe(false)
62
+ expect(result.source).toBe('fallback')
63
+ expect(result.reason).toBe('setup.runtime_check timeout')
64
+ })
65
+ })
@@ -0,0 +1,147 @@
1
+ export interface SetupStatusSnapshot {
2
+ provider_configured?: boolean
3
+ }
4
+
5
+ export interface RuntimeCheckSnapshot {
6
+ error?: string
7
+ ok?: boolean
8
+ }
9
+
10
+ export interface RuntimeReadinessSignals {
11
+ setup: null | SetupStatusSnapshot
12
+ setupError: null | string
13
+ runtime: null | RuntimeCheckSnapshot
14
+ runtimeError: null | string
15
+ }
16
+
17
+ export interface RuntimeReadinessOptions {
18
+ defaultReason?: string
19
+ unknownReady?: boolean
20
+ }
21
+
22
+ export interface RuntimeReadinessResult {
23
+ checksDisagree: boolean
24
+ ready: boolean
25
+ reason: null | string
26
+ source: 'fallback' | 'runtime_check' | 'setup_status'
27
+ }
28
+
29
+ export type RuntimeReadinessRequester = <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
30
+
31
+ const DEFAULT_NOT_READY_REASON = 'Add a provider credential before sending your first message.'
32
+
33
+ function toErrorMessage(error: unknown): null | string {
34
+ if (error instanceof Error) {
35
+ return error.message
36
+ }
37
+
38
+ if (typeof error === 'string') {
39
+ return error
40
+ }
41
+
42
+ if (error === null || error === undefined) {
43
+ return null
44
+ }
45
+
46
+ return String(error)
47
+ }
48
+
49
+ function normalizeMessage(value: null | string | undefined): null | string {
50
+ const next = value?.trim()
51
+
52
+ return next ? next : null
53
+ }
54
+
55
+ async function requestWithFallback<T>(
56
+ requestGateway: RuntimeReadinessRequester,
57
+ method: string
58
+ ): Promise<{ error: null | string; value: null | T }> {
59
+ try {
60
+ return { error: null, value: await requestGateway<T>(method) }
61
+ } catch (error) {
62
+ return { error: toErrorMessage(error), value: null }
63
+ }
64
+ }
65
+
66
+ export async function fetchRuntimeReadinessSignals(
67
+ requestGateway: RuntimeReadinessRequester
68
+ ): Promise<RuntimeReadinessSignals> {
69
+ const [setup, runtime] = await Promise.all([
70
+ requestWithFallback<SetupStatusSnapshot>(requestGateway, 'setup.status'),
71
+ requestWithFallback<RuntimeCheckSnapshot>(requestGateway, 'setup.runtime_check')
72
+ ])
73
+
74
+ return {
75
+ setup: setup.value,
76
+ setupError: setup.error,
77
+ runtime: runtime.value,
78
+ runtimeError: runtime.error
79
+ }
80
+ }
81
+
82
+ export function interpretRuntimeReadiness(
83
+ signals: RuntimeReadinessSignals,
84
+ options: RuntimeReadinessOptions = {}
85
+ ): RuntimeReadinessResult {
86
+ const defaultReason = options.defaultReason ?? DEFAULT_NOT_READY_REASON
87
+ const unknownReady = options.unknownReady ?? false
88
+
89
+ const setupConfigured =
90
+ typeof signals.setup?.provider_configured === 'boolean' ? Boolean(signals.setup.provider_configured) : undefined
91
+
92
+ const runtimeOk = typeof signals.runtime?.ok === 'boolean' ? Boolean(signals.runtime.ok) : undefined
93
+ const runtimeFailure = normalizeMessage(signals.runtime?.error) ?? normalizeMessage(signals.runtimeError)
94
+ const setupFailure = normalizeMessage(signals.setupError)
95
+
96
+ const checksDisagree =
97
+ typeof setupConfigured === 'boolean' && typeof runtimeOk === 'boolean' && setupConfigured !== runtimeOk
98
+
99
+ if (typeof runtimeOk === 'boolean') {
100
+ if (runtimeOk) {
101
+ return {
102
+ checksDisagree,
103
+ ready: true,
104
+ reason: null,
105
+ source: 'runtime_check'
106
+ }
107
+ }
108
+
109
+ let reason = runtimeFailure ?? defaultReason
110
+
111
+ if (checksDisagree && setupConfigured) {
112
+ reason = `${reason} setup.status reports configured credentials, but runtime resolution still failed.`
113
+ }
114
+
115
+ return {
116
+ checksDisagree,
117
+ ready: false,
118
+ reason,
119
+ source: 'runtime_check'
120
+ }
121
+ }
122
+
123
+ if (typeof setupConfigured === 'boolean') {
124
+ return {
125
+ checksDisagree: false,
126
+ ready: setupConfigured,
127
+ reason: setupConfigured ? null : (runtimeFailure ?? setupFailure ?? defaultReason),
128
+ source: 'setup_status'
129
+ }
130
+ }
131
+
132
+ return {
133
+ checksDisagree: false,
134
+ ready: unknownReady,
135
+ reason: unknownReady ? null : (runtimeFailure ?? setupFailure ?? defaultReason),
136
+ source: 'fallback'
137
+ }
138
+ }
139
+
140
+ export async function evaluateRuntimeReadiness(
141
+ requestGateway: RuntimeReadinessRequester,
142
+ options: RuntimeReadinessOptions = {}
143
+ ): Promise<RuntimeReadinessResult> {
144
+ const signals = await fetchRuntimeReadinessSignals(requestGateway)
145
+
146
+ return interpretRuntimeReadiness(signals, options)
147
+ }
@@ -0,0 +1,57 @@
1
+ import type { SessionInfo } from '@/nastech'
2
+ import { getSessionMessages } from '@/nastech'
3
+ import { translateNow } from '@/i18n'
4
+ import { notify, notifyError } from '@/store/notifications'
5
+
6
+ interface ExportSessionParams {
7
+ sessionId: string
8
+ title?: string | null
9
+ session?: SessionInfo
10
+ }
11
+
12
+ function sanitizeFilenamePart(value: string) {
13
+ return value
14
+ .trim()
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9._-]+/g, '-')
17
+ .replace(/^-+|-+$/g, '')
18
+ .slice(0, 48)
19
+ }
20
+
21
+ function sessionExportFilename(sessionId: string, title?: string | null) {
22
+ const titlePart = title ? sanitizeFilenamePart(title) : ''
23
+ const idPart = sanitizeFilenamePart(sessionId).slice(0, 8) || 'session'
24
+
25
+ return `${titlePart || 'session'}-${idPart}.json`
26
+ }
27
+
28
+ export async function exportSession(sessionId: string, params: Omit<ExportSessionParams, 'sessionId'> = {}) {
29
+ if (!sessionId) {
30
+ return
31
+ }
32
+
33
+ try {
34
+ const { messages } = await getSessionMessages(sessionId)
35
+
36
+ const payload = {
37
+ exported_at: new Date().toISOString(),
38
+ session_id: sessionId,
39
+ title: params.title ?? null,
40
+ session: params.session ?? null,
41
+ message_count: messages.length,
42
+ messages
43
+ }
44
+
45
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
46
+ const downloadUrl = URL.createObjectURL(blob)
47
+ const anchor = document.createElement('a')
48
+ anchor.href = downloadUrl
49
+ anchor.download = sessionExportFilename(sessionId, params.title)
50
+ anchor.click()
51
+ URL.revokeObjectURL(downloadUrl)
52
+
53
+ notify({ kind: 'success', message: translateNow('desktop.sessionExported'), durationMs: 2_000 })
54
+ } catch (err) {
55
+ notifyError(err, translateNow('desktop.sessionExportFailed'))
56
+ }
57
+ }