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,386 @@
1
+ import { isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code'
2
+ import { stripPreviewTargets } from '@/lib/preview-targets'
3
+
4
+ const REASONING_BLOCK_RE = /<(think|thinking|reasoning|scratchpad|analysis)>[\s\S]*?<\/\1>\s*/gi
5
+ const PREVIEW_MARKER_RE = /\[Preview:[^\]]+\]\(#preview[:/][^)]+\)/gi
6
+
7
+ const FENCE_LINE_RE = /^([ \t]*)(`{3,}|~{3,})([^\n]*)$/
8
+ const EMPTY_FENCE_BLOCK_RE = /(^|\n)[ \t]*(?:`{3,}|~{3,})[^\n]*\n[ \t]*(?:`{3,}|~{3,})[ \t]*(?=\n|$)/g
9
+ const CODE_FENCE_SPLIT_RE = /((?:```|~~~)[\s\S]*?(?:```|~~~))/g
10
+ const INLINE_CODE_SPLIT_RE = /(`[^`\n]+`)/g
11
+ // Bare-URL autolink matcher. The character classes EXCLUDE `*` so a URL that
12
+ // abuts markdown emphasis with no separating space (e.g. `**label: https://x**`,
13
+ // a very common LLM pattern) doesn't swallow the trailing `**` into the href.
14
+ // `*` is never meaningful in a real URL path, and GFM's own autolink extension
15
+ // likewise strips trailing emphasis/punctuation — so dropping it here is safe
16
+ // and keeps the emphasis run intact. Other trailing punctuation is still peeled
17
+ // off by the final `[^\s<>"'`*.,;:!?]` class.
18
+ const RAW_URL_RE = /https?:\/\/[^\s<>"'`*]+[^\s<>"'`*.,;:!?]/g
19
+ const LOCAL_PREVIEW_URL_RE = /(^|\s)https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?[^\s<>"'`]*/gi
20
+ const LOCAL_PREVIEW_ONLY_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i
21
+ const URL_ONLY_LINE_RE = /^\s*https?:\/\/\S+\s*$/i
22
+ const CITATION_MARKER_RE = /(?<=[\p{L}\p{N})\].,!?:;"'”’])\[(?:\d+(?:\s*,\s*\d+)*)\](?!\()/gu
23
+
24
+ /**
25
+ * Returns true when `body` contains a line that's exactly `marker` (modulo
26
+ * leading/trailing horizontal whitespace) — i.e. an unambiguous close fence
27
+ * for an opening fence with the same marker.
28
+ *
29
+ * Implemented with string comparisons (not RegExp) so that input-derived
30
+ * `marker` values can never bleed into a regex pattern. This matters for
31
+ * CodeQL's `js/incomplete-hostname-regexp` dataflow, which would otherwise
32
+ * trace test-fixture URLs from the input through `marker` into the regex
33
+ * source, even though `marker` is captured by `(`{3,}|~{3,})` and can only
34
+ * ever be backticks or tildes.
35
+ */
36
+ function hasCloseFenceLine(body: string, marker: string): boolean {
37
+ const lines = body.split('\n')
38
+
39
+ // Original regex required `\n` immediately before the close fence, so the
40
+ // first line of `body` (which has no preceding newline within `body`)
41
+ // cannot itself be the close fence.
42
+ for (let i = 1; i < lines.length; i += 1) {
43
+ const line = lines[i]
44
+ let lo = 0
45
+ let hi = line.length
46
+
47
+ while (lo < hi && (line[lo] === ' ' || line[lo] === '\t')) {
48
+ lo += 1
49
+ }
50
+
51
+ while (hi > lo && (line[hi - 1] === ' ' || line[hi - 1] === '\t')) {
52
+ hi -= 1
53
+ }
54
+
55
+ if (line.slice(lo, hi) === marker) {
56
+ return true
57
+ }
58
+ }
59
+
60
+ return false
61
+ }
62
+
63
+ function scrubBacktickNoise(text: string): string {
64
+ const balancedFenceRe = /(^|\n)([ \t]*)(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n[ \t]*\3[ \t]*(?=\n|$)/g
65
+ const protectedRanges: { end: number; start: number }[] = []
66
+ let match: RegExpExecArray | null
67
+
68
+ while ((match = balancedFenceRe.exec(text)) !== null) {
69
+ const start = match.index + match[1].length
70
+
71
+ protectedRanges.push({ end: balancedFenceRe.lastIndex, start })
72
+ }
73
+
74
+ const danglingCodeFenceRe = /(^|\n)[ \t]*(`{3,}|~{3,})([a-z0-9][a-z0-9+#-]{0,15})[ \t]*\n([\s\S]*)$/gi
75
+
76
+ while ((match = danglingCodeFenceRe.exec(text)) !== null) {
77
+ const start = match.index + match[1].length
78
+ const marker = match[2] || '```'
79
+ const info = match[3] || ''
80
+ const body = match[4] || ''
81
+
82
+ if (!hasCloseFenceLine(body, marker) && sanitizeLanguageTag(info) && !isLikelyProseFence(info, body)) {
83
+ protectedRanges.push({ end: text.length, start })
84
+
85
+ break
86
+ }
87
+ }
88
+
89
+ protectedRanges.sort((a, b) => a.start - b.start)
90
+
91
+ const fenceNoiseRe = /`{3,}/g
92
+ let out = ''
93
+ let cursor = 0
94
+
95
+ for (const range of protectedRanges) {
96
+ out += text.slice(cursor, range.start).replace(fenceNoiseRe, '')
97
+ out += text.slice(range.start, range.end)
98
+ cursor = range.end
99
+ }
100
+
101
+ out += text.slice(cursor).replace(fenceNoiseRe, '')
102
+
103
+ for (let pass = 0; pass < 2; pass += 1) {
104
+ // Match EXACTLY 2 backticks (not part of a longer run) on each side.
105
+ // Without the lookbehind/lookahead, two adjacent triple-backtick
106
+ // fences with only whitespace between them get spliced together —
107
+ // e.g. ```bash\n...\n```\n\n```latex matches the regex's
108
+ // last-2-of-bash-close + \n\n + first-2-of-latex-open and the
109
+ // surrounding fence markers collapse into a single longer block,
110
+ // which the markdown parser then treats as ONE giant code block.
111
+ out = out.replace(/(?<!`)``(?!`)\s*(?<!`)``(?!`)/g, '')
112
+ out = out.replace(/(^|[^`])``(?=\s|[.,;:!?)\]'"\u2014\u2013-]|$)/g, '$1')
113
+ }
114
+
115
+ return out
116
+ }
117
+
118
+ function stripEmptyFenceBlocks(text: string): string {
119
+ return text.replace(EMPTY_FENCE_BLOCK_RE, '$1')
120
+ }
121
+
122
+ function isUrlOnlyBlock(lines: string[]): boolean {
123
+ const nonEmpty = lines.filter(line => line.trim())
124
+
125
+ return nonEmpty.length > 0 && nonEmpty.every(line => URL_ONLY_LINE_RE.test(line))
126
+ }
127
+
128
+ function autoLinkRawUrls(text: string): string {
129
+ return text.replace(RAW_URL_RE, (url: string, index: number) => {
130
+ const previous = text[index - 1] || ''
131
+ const beforePrevious = text[index - 2] || ''
132
+
133
+ if (previous === '<' || (beforePrevious === ']' && previous === '(')) {
134
+ return url
135
+ }
136
+
137
+ return `<${url}>`
138
+ })
139
+ }
140
+
141
+ function normalizeVisibleProse(text: string): string {
142
+ return text
143
+ .split(INLINE_CODE_SPLIT_RE)
144
+ .map(part =>
145
+ part.startsWith('`')
146
+ ? part
147
+ : autoLinkRawUrls(
148
+ part.replace(/`{3,}/g, '').replace(LOCAL_PREVIEW_URL_RE, '$1').replace(CITATION_MARKER_RE, '')
149
+ )
150
+ )
151
+ .join('')
152
+ }
153
+
154
+ function pushProseFence(out: string[], indent: string, info: string, lines: string[]) {
155
+ if (info) {
156
+ out.push(`${indent}${info}`.trimEnd())
157
+ }
158
+
159
+ out.push(...lines)
160
+ }
161
+
162
+ function findClosingFence(lines: string[], start: number, marker: string): number {
163
+ for (let cursor = start + 1; cursor < lines.length; cursor += 1) {
164
+ const closeMatch = (lines[cursor] || '').match(FENCE_LINE_RE)
165
+
166
+ if (!closeMatch) {
167
+ continue
168
+ }
169
+
170
+ const closeMarker = closeMatch[2] || ''
171
+ const closeInfo = (closeMatch[3] || '').trim()
172
+
173
+ if (!closeInfo && closeMarker[0] === marker[0] && closeMarker.length >= marker.length) {
174
+ return cursor
175
+ }
176
+ }
177
+
178
+ return -1
179
+ }
180
+
181
+ // Languages that should be routed to the math (KaTeX) renderer instead of
182
+ // being shown as a syntax-highlighted code block.
183
+ //
184
+ // We deliberately recognize ONLY `math` here, not `latex` or `tex`.
185
+ // Reasoning: GitHub-style markdown uses ` ```math ` to mean "render as
186
+ // math" and ` ```latex `/` ```tex ` to mean "show LaTeX/TeX source code"
187
+ // (syntax highlighted). Conflating the two breaks code blocks where a
188
+ // user is *discussing* LaTeX rather than embedding it (e.g.,
189
+ // ```latex\n\begin{equation}\n E = mc^2\n\end{equation}``` shown as a
190
+ // teaching example). Anyone who wants math rendered should use ```math.
191
+ const MATH_FENCE_LANGUAGES = new Set(['math'])
192
+
193
+ function isMathFence(language: string): boolean {
194
+ return MATH_FENCE_LANGUAGES.has(language.toLowerCase())
195
+ }
196
+
197
+ function normalizeFenceBlocks(text: string): string {
198
+ const sourceLines = text.split('\n')
199
+ const out: string[] = []
200
+ let index = 0
201
+
202
+ while (index < sourceLines.length) {
203
+ const line = sourceLines[index] || ''
204
+ const match = line.match(FENCE_LINE_RE)
205
+
206
+ if (!match) {
207
+ out.push(line)
208
+ index += 1
209
+
210
+ continue
211
+ }
212
+
213
+ const indent = match[1] || ''
214
+ const marker = match[2] || '```'
215
+ const infoRaw = (match[3] || '').trim()
216
+ const languageToken = infoRaw.split(/\s+/, 1)[0] || ''
217
+ const language = sanitizeLanguageTag(languageToken)
218
+ const openerValid = !infoRaw || Boolean(language)
219
+
220
+ if (!openerValid) {
221
+ out.push(`${indent}${infoRaw}`.trimEnd())
222
+ index += 1
223
+
224
+ continue
225
+ }
226
+
227
+ const closeIndex = findClosingFence(sourceLines, index, marker)
228
+ const bodyLines = sourceLines.slice(index + 1, closeIndex === -1 ? sourceLines.length : closeIndex)
229
+ const body = bodyLines.join('\n')
230
+
231
+ if (closeIndex !== -1 && !body.trim()) {
232
+ index = closeIndex + 1
233
+
234
+ continue
235
+ }
236
+
237
+ if (closeIndex !== -1 && LOCAL_PREVIEW_ONLY_RE.test(body.trim())) {
238
+ index = closeIndex + 1
239
+
240
+ continue
241
+ }
242
+
243
+ if (closeIndex !== -1 && isUrlOnlyBlock(bodyLines)) {
244
+ out.push(...bodyLines)
245
+ index = closeIndex + 1
246
+
247
+ continue
248
+ }
249
+
250
+ if (closeIndex === -1) {
251
+ if (!body.trim()) {
252
+ index += 1
253
+
254
+ continue
255
+ }
256
+
257
+ if (isLikelyProseFence(infoRaw, body)) {
258
+ pushProseFence(out, indent, infoRaw, bodyLines)
259
+ } else if (isMathFence(language)) {
260
+ // Streaming math fence — rewrite the language tag to "math".
261
+ // remark-math + rehype-katex pick up ```math fenced blocks via
262
+ // the language-math class on the resulting <code> element. We
263
+ // keep the fence intact (instead of converting to $$..$$) so
264
+ // any literal `$$` characters in the body don't collide with
265
+ // an outer math wrapper. No close emitted yet — streaming.
266
+ out.push(`${indent}${marker}math`)
267
+ out.push(...bodyLines)
268
+ } else {
269
+ out.push(`${indent}${marker}${language}`)
270
+ out.push(...bodyLines)
271
+ }
272
+
273
+ break
274
+ }
275
+
276
+ if (isLikelyProseFence(infoRaw, body)) {
277
+ pushProseFence(out, indent, infoRaw, bodyLines)
278
+ index = closeIndex + 1
279
+
280
+ continue
281
+ }
282
+
283
+ if (isMathFence(language)) {
284
+ // Closed math fence — rewrite the language tag to "math" so
285
+ // rehype-katex's language-math class detection picks it up.
286
+ // Body stays untouched (no $$..$$ rewrite) so authors can write
287
+ // arbitrary LaTeX including `$$display$$` markers without them
288
+ // colliding with our wrapper. Without this rewrite the block
289
+ // would render as a syntax-highlighted "latex" code listing.
290
+ out.push(`${indent}${marker}math`)
291
+ out.push(...bodyLines)
292
+ out.push(`${indent}${marker}`)
293
+ index = closeIndex + 1
294
+
295
+ continue
296
+ }
297
+
298
+ out.push(`${indent}${marker}${language}`)
299
+ out.push(...bodyLines)
300
+ out.push(`${indent}${marker}`)
301
+ index = closeIndex + 1
302
+ }
303
+
304
+ return out.join('\n')
305
+ }
306
+
307
+ // Convert LaTeX bracket delimiters to remark-math's dollar-sign syntax.
308
+ // Models often emit `\(...\)` for inline math and `\[...\]` for display
309
+ // math (the standard LaTeX convention) instead of `$...$` / `$$...$$`.
310
+ // remark-math only natively recognizes the dollar form, so we rewrite at
311
+ // preprocess time. Done with simple non-greedy matches keyed on the
312
+ // escaped-bracket sequences — these are rare enough in non-math content
313
+ // (you'd have to write a literal `\(` followed eventually by a literal
314
+ // `\)` with NO interleaving newline-paragraph-break) that false positives
315
+ // are extremely unlikely.
316
+ const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g
317
+ const LATEX_DISPLAY_RE = /\\\[([\s\S]+?)\\\]/g
318
+
319
+ function rewriteLatexBracketDelimiters(text: string): string {
320
+ return text
321
+ .replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`)
322
+ .replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
323
+ }
324
+
325
+ // Escape `$<digit>` patterns so they don't get eaten as math delimiters.
326
+ // Models commonly write currency amounts ($5, $19.99, $1,299) in prose.
327
+ // With `singleDollarTextMath: true`, remark-math is greedy and matches
328
+ // EVERY pair of `$`s — including the open of `$5` to the next `$10`,
329
+ // rendering "5 in my pocket and you have " as italicized math text.
330
+ // The de-facto convention across math-supporting LLM UIs is to treat
331
+ // `$` followed by a digit as currency rather than math, since math
332
+ // expressions almost always start with a letter or `\command`. Trade-
333
+ // off: a math expression like `$5x = 10$` would have its leading 5
334
+ // escaped — annoying but rare. The escape `\$` survives to render as
335
+ // a literal `$` in the final output.
336
+ const CURRENCY_DOLLAR_RE = /(^|[^\\])\$(?=\d)/g
337
+
338
+ function escapeCurrencyDollars(text: string): string {
339
+ return text.replace(CURRENCY_DOLLAR_RE, '$1\\$')
340
+ }
341
+
342
+ export function preprocessMarkdown(text: string): string {
343
+ const cleaned = text.replace(REASONING_BLOCK_RE, '').replace(PREVIEW_MARKER_RE, '')
344
+ const scrubbed = scrubBacktickNoise(cleaned)
345
+ const normalizedFences = normalizeFenceBlocks(scrubbed)
346
+ const strippedEmptyFences = stripEmptyFenceBlocks(normalizedFences)
347
+
348
+ return strippedEmptyFences
349
+ .split(CODE_FENCE_SPLIT_RE)
350
+ .map(part => {
351
+ // Fence blocks pass through untouched.
352
+ if (/^(?:```|~~~)/.test(part)) {
353
+ return part
354
+ }
355
+
356
+ // Whitespace-only segments (e.g. the `\n\n` between two adjacent
357
+ // fences) must NOT go through stripPreviewTargets — its internal
358
+ // .trim() would collapse them to '' and glue the surrounding
359
+ // fences together, producing things like ``````math which the
360
+ // markdown parser then reads as a single 6-backtick block.
361
+ if (!part.trim()) {
362
+ return part
363
+ }
364
+
365
+ // Preserve leading/trailing whitespace around the prose body so
366
+ // that fence-prose-fence sequences keep their blank-line gaps.
367
+ // stripPreviewTargets internally calls .trim() on its result for
368
+ // the benefit of its other (single-segment) callers; here we're
369
+ // operating on a SEGMENT of a larger document where outer
370
+ // whitespace is structural and must survive.
371
+ const leading = part.match(/^\s*/)?.[0] ?? ''
372
+ const trailing = part.match(/\s*$/)?.[0] ?? ''
373
+
374
+ // rewriteLatexBracketDelimiters runs only on prose segments so
375
+ // we don't accidentally touch `\(` inside a code block.
376
+ // escapeCurrencyDollars likewise only runs on prose, so legit
377
+ // `$5` literals inside fenced code stay intact.
378
+ const transformed = normalizeVisibleProse(
379
+ stripPreviewTargets(rewriteLatexBracketDelimiters(escapeCurrencyDollars(part)))
380
+ )
381
+
382
+ return leading + transformed + trailing
383
+ })
384
+ .join('')
385
+ .replace(/[ \t]+\n/g, '\n')
386
+ }
@@ -0,0 +1,58 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { $connection } from '@/store/session'
4
+
5
+ import { filePathFromMediaPath, gatewayMediaDataUrl, isRemoteGateway } from './media'
6
+
7
+ describe('isRemoteGateway', () => {
8
+ afterEach(() => {
9
+ $connection.set(null)
10
+ })
11
+
12
+ it('is false with no connection', () => {
13
+ $connection.set(null)
14
+ expect(isRemoteGateway()).toBe(false)
15
+ })
16
+
17
+ it('is false in local mode', () => {
18
+ $connection.set({ mode: 'local' } as never)
19
+ expect(isRemoteGateway()).toBe(false)
20
+ })
21
+
22
+ it('is true in remote mode', () => {
23
+ $connection.set({ mode: 'remote' } as never)
24
+ expect(isRemoteGateway()).toBe(true)
25
+ })
26
+ })
27
+
28
+ describe('filePathFromMediaPath', () => {
29
+ it('passes through a plain path', () => {
30
+ expect(filePathFromMediaPath('/home/u/.nastech/images/a.png')).toBe('/home/u/.nastech/images/a.png')
31
+ })
32
+
33
+ it('decodes a file:// URL with encoded characters', () => {
34
+ expect(filePathFromMediaPath('file:///tmp/a%20b.png')).toBe('/tmp/a b.png')
35
+ })
36
+ })
37
+
38
+ describe('gatewayMediaDataUrl', () => {
39
+ const api = vi.fn(async () => ({ data_url: 'data:image/png;base64,ZHVtbXk=' }))
40
+
41
+ beforeEach(() => {
42
+ api.mockClear()
43
+ vi.stubGlobal('window', { NASTECHDesktop: { api } })
44
+ })
45
+
46
+ afterEach(() => {
47
+ vi.unstubAllGlobals()
48
+ })
49
+
50
+ it('requests the encoded gateway path and returns the data URL', async () => {
51
+ const url = await gatewayMediaDataUrl('/home/u/.nastech/images/a b.png')
52
+
53
+ expect(url).toBe('data:image/png;base64,ZHVtbXk=')
54
+ expect(api).toHaveBeenCalledWith({
55
+ path: '/api/media?path=%2Fhome%2Fu%2F.nastech%2Fimages%2Fa%20b.png'
56
+ })
57
+ })
58
+ })
@@ -0,0 +1,111 @@
1
+ import { $connection } from '@/store/session'
2
+
3
+ export type MediaKind = 'audio' | 'image' | 'video' | 'file'
4
+
5
+ interface MediaInfo {
6
+ kind: MediaKind
7
+ mime: string
8
+ }
9
+
10
+ const MEDIA_BY_EXT: Record<string, MediaInfo> = {
11
+ avi: { kind: 'video', mime: 'video/x-msvideo' },
12
+ bmp: { kind: 'image', mime: 'image/bmp' },
13
+ flac: { kind: 'audio', mime: 'audio/flac' },
14
+ gif: { kind: 'image', mime: 'image/gif' },
15
+ jpeg: { kind: 'image', mime: 'image/jpeg' },
16
+ jpg: { kind: 'image', mime: 'image/jpeg' },
17
+ m4a: { kind: 'audio', mime: 'audio/mp4' },
18
+ mkv: { kind: 'video', mime: 'video/x-matroska' },
19
+ mov: { kind: 'video', mime: 'video/quicktime' },
20
+ mp3: { kind: 'audio', mime: 'audio/mpeg' },
21
+ mp4: { kind: 'video', mime: 'video/mp4' },
22
+ ogg: { kind: 'audio', mime: 'audio/ogg' },
23
+ opus: { kind: 'audio', mime: 'audio/ogg; codecs=opus' },
24
+ png: { kind: 'image', mime: 'image/png' },
25
+ svg: { kind: 'image', mime: 'image/svg+xml' },
26
+ wav: { kind: 'audio', mime: 'audio/wav' },
27
+ webm: { kind: 'video', mime: 'video/webm' },
28
+ webp: { kind: 'image', mime: 'image/webp' }
29
+ }
30
+
31
+ function mediaInfo(path: string): MediaInfo | undefined {
32
+ const ext = path.split(/[?#]/, 1)[0]?.split('.').pop()?.toLowerCase()
33
+
34
+ return ext ? MEDIA_BY_EXT[ext] : undefined
35
+ }
36
+
37
+ export function mediaKind(path: string): MediaKind {
38
+ return mediaInfo(path)?.kind ?? 'file'
39
+ }
40
+
41
+ export function mediaMime(path: string): string {
42
+ return mediaInfo(path)?.mime ?? 'application/octet-stream'
43
+ }
44
+
45
+ export function mediaName(path: string): string {
46
+ try {
47
+ const url = new URL(path)
48
+
49
+ return url.pathname.split('/').filter(Boolean).pop() || path
50
+ } catch {
51
+ return path.split(/[\\/]/).filter(Boolean).pop() || path
52
+ }
53
+ }
54
+
55
+ export function mediaMarkdownHref(path: string): string {
56
+ return `#media:${encodeURIComponent(path)}`
57
+ }
58
+
59
+ export function mediaExternalUrl(path: string): string {
60
+ return /^(?:https?|file):/i.test(path) ? path : `file://${path}`
61
+ }
62
+
63
+ // Custom Electron scheme (registered in electron/main.cjs) that streams a local
64
+ // file with Range support. Used for audio/video so playback bypasses the data
65
+ // URL size cap and supports seeking. `path` may be a plain path or `file://…`.
66
+ export function mediaStreamUrl(path: string): string {
67
+ return `NASTECH-media://stream/${encodeURIComponent(filePathFromMediaPath(path))}`
68
+ }
69
+
70
+ export function mediaPathFromMarkdownHref(href?: string): string | null {
71
+ if (!href?.startsWith('#media:')) {
72
+ return null
73
+ }
74
+
75
+ try {
76
+ return decodeURIComponent(href.slice('#media:'.length))
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ export function filePathFromMediaPath(path: string): string {
83
+ if (!path.startsWith('file:')) {
84
+ return path
85
+ }
86
+
87
+ try {
88
+ return decodeURIComponent(new URL(path).pathname)
89
+ } catch {
90
+ return path.replace(/^file:\/\//, '')
91
+ }
92
+ }
93
+
94
+ export function mediaDisplayLabel(path: string): string {
95
+ const escaped = mediaName(path).replace(/[[\]\\]/g, '\\$&')
96
+ const kind = mediaKind(path)
97
+
98
+ return `${kind[0].toUpperCase()}${kind.slice(1)}: ${escaped}`
99
+ }
100
+
101
+ export function isRemoteGateway(): boolean {
102
+ return $connection.get()?.mode === 'remote'
103
+ }
104
+
105
+ export async function gatewayMediaDataUrl(path: string): Promise<string> {
106
+ const result = await window.NASTECHDesktop.api<{ data_url: string }>({
107
+ path: `/api/media?path=${encodeURIComponent(path)}`
108
+ })
109
+
110
+ return result.data_url
111
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { displayModelName, formatModelStatusLabel, reasoningEffortLabel } from './model-status-label'
4
+
5
+ describe('model-status-label', () => {
6
+ it('formats display names consistently', () => {
7
+ expect(displayModelName('anthropic/claude-opus-4.8-fast')).toBe('Opus 4.8')
8
+ expect(displayModelName('openai/gpt-5.5')).toBe('GPT-5.5')
9
+ })
10
+
11
+ it('maps reasoning effort to compact labels', () => {
12
+ expect(reasoningEffortLabel('high')).toBe('High')
13
+ expect(reasoningEffortLabel('xhigh')).toBe('Max')
14
+ expect(reasoningEffortLabel('')).toBe('')
15
+ })
16
+
17
+ it('appends fast + effort session state to the status label', () => {
18
+ expect(formatModelStatusLabel('openai/gpt-5.5', { fastMode: true, reasoningEffort: 'high' })).toBe(
19
+ 'GPT-5.5 · Fast High'
20
+ )
21
+ })
22
+
23
+ it('always surfaces the effort (default medium) so the level is visible', () => {
24
+ expect(formatModelStatusLabel('openai/gpt-5.5', { reasoningEffort: 'medium' })).toBe('GPT-5.5 · Med')
25
+ expect(formatModelStatusLabel('openai/gpt-5.5')).toBe('GPT-5.5 · Med')
26
+ })
27
+
28
+ it('returns just the placeholder name when there is no model', () => {
29
+ expect(formatModelStatusLabel('')).toBe('No model')
30
+ })
31
+ })
@@ -0,0 +1,103 @@
1
+ const REASONING_LABELS: Record<string, string> = {
2
+ none: 'Off',
3
+ minimal: 'Min',
4
+ low: 'Low',
5
+ medium: 'Med',
6
+ high: 'High',
7
+ xhigh: 'Max'
8
+ }
9
+
10
+ export function reasoningEffortLabel(effort: string): string {
11
+ const key = effort.trim().toLowerCase()
12
+
13
+ if (!key) {
14
+ return ''
15
+ }
16
+
17
+ return REASONING_LABELS[key] ?? effort
18
+ }
19
+
20
+ /** Strip provider prefix and normalize for display. */
21
+ export function modelBaseId(model: string): string {
22
+ const trimmed = model.trim()
23
+ const slash = trimmed.lastIndexOf('/')
24
+
25
+ return slash >= 0 ? trimmed.slice(slash + 1) : trimmed
26
+ }
27
+
28
+ // Trailing model-id variants that should render as a grayed tag beside the
29
+ // name (e.g. "Opus 4.8" + "Fast") rather than collapsing two distinct ids to
30
+ // the same display name.
31
+ const VARIANT_TAGS: ReadonlyArray<readonly [RegExp, string]> = [
32
+ [/-fast$/i, 'Fast'],
33
+ [/-thinking$/i, 'Thinking'],
34
+ [/-preview$/i, 'Preview'],
35
+ [/-latest$/i, 'Latest']
36
+ ]
37
+
38
+ const titleCase = (text: string): string => text.replace(/\b\w/g, char => char.toUpperCase()).trim()
39
+
40
+ function prettifyBase(base: string): string {
41
+ if (/^claude-/i.test(base)) {
42
+ return titleCase(base.replace(/^claude-/i, '').replace(/-/g, ' '))
43
+ }
44
+
45
+ if (/^gpt-/i.test(base)) {
46
+ return base.replace(/^gpt-/i, 'GPT-')
47
+ }
48
+
49
+ if (/^gemini-/i.test(base)) {
50
+ return base.replace(/^gemini-/i, 'Gemini ').replace(/-/g, ' ')
51
+ }
52
+
53
+ return titleCase(base.replace(/-/g, ' '))
54
+ }
55
+
56
+ /** Split a model id into a clean display name plus an optional grayed variant
57
+ * tag, so distinct ids (e.g. `…-4.8` vs `…-4.8-fast`) don't collapse. */
58
+ export function modelDisplayParts(model: string): { name: string; tag: string } {
59
+ let base = modelBaseId(model)
60
+ let tag = ''
61
+
62
+ for (const [pattern, label] of VARIANT_TAGS) {
63
+ if (pattern.test(base)) {
64
+ tag = label
65
+ base = base.replace(pattern, '')
66
+
67
+ break
68
+ }
69
+ }
70
+
71
+ return { name: prettifyBase(base) || model.trim() || 'No model', tag }
72
+ }
73
+
74
+ /** Friendly one-line model name for menus and the status bar. */
75
+ export function displayModelName(model: string): string {
76
+ return modelDisplayParts(model).name
77
+ }
78
+
79
+ /** Status bar trigger label — model name plus the live session state (effort/fast). */
80
+ export function formatModelStatusLabel(
81
+ model: string,
82
+ options?: { fastMode?: boolean; reasoningEffort?: string }
83
+ ): string {
84
+ const name = displayModelName(model)
85
+
86
+ if (!model.trim()) {
87
+ return name
88
+ }
89
+
90
+ const parts: string[] = []
91
+
92
+ // Fast is shown when the speed=fast param is on (options.fastMode) OR the
93
+ // active model is a `…-fast` variant (fast via a separate model id).
94
+ if (options?.fastMode || /-fast$/i.test(modelBaseId(model))) {
95
+ parts.push('Fast')
96
+ }
97
+
98
+ // Always surface the effort (empty = NasTech default of medium) so the
99
+ // current reasoning level is visible at a glance, not just when non-default.
100
+ parts.push(reasoningEffortLabel(options?.reasoningEffort ?? '') || 'Med')
101
+
102
+ return `${name} · ${parts.join(' ')}`
103
+ }
@@ -0,0 +1,6 @@
1
+ import type { MutableRefObject } from 'react'
2
+
3
+ /** Imperative ref write — extracted so react-compiler doesn't flag hook-arg refs. */
4
+ export function setMutableRef<T>(ref: MutableRefObject<T>, value: T) {
5
+ ref.current = value
6
+ }