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,559 @@
1
+ import type * as React from 'react'
2
+ import type {
3
+ ComponentProps,
4
+ CSSProperties,
5
+ DragEvent as ReactDragEvent,
6
+ MouseEvent as ReactMouseEvent,
7
+ ReactNode
8
+ } from 'react'
9
+ import { useEffect, useMemo, useState } from 'react'
10
+ import ShikiHighlighter from 'react-shiki'
11
+ import { Streamdown } from 'streamdown'
12
+
13
+ import { NASTECH_PATHS_MIME } from '@/app/chat/hooks/use-composer-actions'
14
+ import { PageLoader } from '@/components/page-loader'
15
+ import { translateNow, useI18n } from '@/i18n'
16
+ import { cn } from '@/lib/utils'
17
+ import type { PreviewTarget } from '@/store/preview'
18
+
19
+ const SHIKI_THEME = { dark: 'github-dark-default', light: 'github-light-default' } as const
20
+ const TEXT_PREVIEW_MAX_BYTES = 512 * 1024
21
+
22
+ type EmptyStateTone = 'neutral' | 'warning'
23
+
24
+ const TONE_STYLES: Record<EmptyStateTone, { cube: string; primary: string }> = {
25
+ neutral: {
26
+ cube: 'text-muted-foreground/35',
27
+ primary: 'border-border bg-background text-foreground hover:bg-accent'
28
+ },
29
+ warning: {
30
+ cube: 'text-amber-500/70 dark:text-amber-300/70',
31
+ primary:
32
+ 'border-amber-400/40 bg-amber-50 text-amber-900 hover:bg-amber-100 dark:border-amber-300/30 dark:bg-amber-300/15 dark:text-amber-100 dark:hover:bg-amber-300/20'
33
+ }
34
+ }
35
+
36
+ function PreviewCubeIcon({ className }: { className?: string }) {
37
+ return (
38
+ <svg aria-hidden="true" className={cn('size-16', className)} viewBox="0 0 64 64">
39
+ <path
40
+ d="M32 5 56 18.5v27L32 59 8 45.5v-27L32 5Z"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ strokeLinejoin="round"
44
+ strokeWidth="1.25"
45
+ />
46
+ <path
47
+ d="M8 18.5 32 32l24-13.5M32 32v27"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ strokeLinejoin="round"
51
+ strokeWidth="1.25"
52
+ />
53
+ <path d="M20 11.75 44 25.25" fill="none" opacity="0.45" stroke="currentColor" strokeWidth="0.9" />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ interface PreviewEmptyStateProps {
59
+ body?: ReactNode
60
+ consoleHeight?: number
61
+ primaryAction?: { disabled?: boolean; label: string; onClick: () => void }
62
+ secondaryAction?: { disabled?: boolean; label: string; onClick: () => void }
63
+ title: string
64
+ tone?: EmptyStateTone
65
+ }
66
+
67
+ export function PreviewEmptyState({
68
+ body,
69
+ consoleHeight = 0,
70
+ primaryAction,
71
+ secondaryAction,
72
+ title,
73
+ tone = 'neutral'
74
+ }: PreviewEmptyStateProps) {
75
+ const styles = TONE_STYLES[tone]
76
+
77
+ return (
78
+ <div
79
+ className="absolute inset-x-0 top-0 z-10 grid place-items-center bg-background px-8 py-10 text-center bottom-(--preview-error-bottom)"
80
+ style={{ '--preview-error-bottom': `${consoleHeight}px` } as CSSProperties}
81
+ >
82
+ <div className="grid max-w-sm justify-items-center gap-5">
83
+ <PreviewCubeIcon className={styles.cube} />
84
+ <div className="grid gap-2">
85
+ <div className="text-sm font-medium text-foreground">{title}</div>
86
+ {body && <div className="text-xs leading-relaxed text-muted-foreground">{body}</div>}
87
+ </div>
88
+ {(primaryAction || secondaryAction) && (
89
+ <div className="grid justify-items-center gap-2">
90
+ {primaryAction && (
91
+ <button
92
+ className={cn(
93
+ 'rounded-full border px-3.5 py-1.5 text-xs font-medium shadow-xs transition-colors disabled:cursor-default disabled:opacity-60',
94
+ styles.primary
95
+ )}
96
+ disabled={primaryAction.disabled}
97
+ onClick={primaryAction.onClick}
98
+ type="button"
99
+ >
100
+ {primaryAction.label}
101
+ </button>
102
+ )}
103
+ {secondaryAction && (
104
+ <button
105
+ className="text-[0.6875rem] font-medium text-muted-foreground underline decoration-current/20 underline-offset-4 transition-colors hover:text-foreground disabled:cursor-default disabled:text-muted-foreground/55 disabled:no-underline"
106
+ disabled={secondaryAction.disabled}
107
+ onClick={secondaryAction.onClick}
108
+ type="button"
109
+ >
110
+ {secondaryAction.label}
111
+ </button>
112
+ )}
113
+ </div>
114
+ )}
115
+ </div>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ interface LocalPreviewState {
121
+ binary?: boolean
122
+ byteSize?: number
123
+ dataUrl?: string
124
+ error?: string
125
+ language?: string
126
+ loading: boolean
127
+ text?: string
128
+ truncated?: boolean
129
+ }
130
+
131
+ function filePathForTarget(target: PreviewTarget) {
132
+ if (target.path) {
133
+ return target.path
134
+ }
135
+
136
+ try {
137
+ const url = new URL(target.url)
138
+
139
+ return url.protocol === 'file:' ? decodeURIComponent(url.pathname) : target.url
140
+ } catch {
141
+ return target.url
142
+ }
143
+ }
144
+
145
+ function formatBytes(bytes: number | undefined) {
146
+ if (!bytes) {
147
+ return translateNow('preview.unknownSize')
148
+ }
149
+
150
+ const units = ['B', 'KB', 'MB', 'GB']
151
+ let value = bytes
152
+ let unit = 0
153
+
154
+ while (value >= 1024 && unit < units.length - 1) {
155
+ value /= 1024
156
+ unit += 1
157
+ }
158
+
159
+ return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`
160
+ }
161
+
162
+ function looksBinaryBytes(bytes: Uint8Array) {
163
+ if (!bytes.length) {
164
+ return false
165
+ }
166
+
167
+ let suspicious = 0
168
+
169
+ for (const byte of bytes.slice(0, 4096)) {
170
+ if (byte === 0) {
171
+ return true
172
+ }
173
+
174
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
175
+ suspicious += 1
176
+ }
177
+ }
178
+
179
+ return suspicious / Math.min(bytes.length, 4096) > 0.12
180
+ }
181
+
182
+ async function readTextPreview(filePath: string) {
183
+ if (window.NASTECHDesktop.readFileText) {
184
+ try {
185
+ return await window.NASTECHDesktop.readFileText(filePath)
186
+ } catch (error) {
187
+ const message = error instanceof Error ? error.message : String(error)
188
+
189
+ if (!message.includes("No handler registered for 'NASTECH:readFileText'")) {
190
+ throw error
191
+ }
192
+ }
193
+ }
194
+
195
+ // Back-compat for a running Electron process whose preload hasn't been
196
+ // restarted since readFileText was added. readFileDataUrl already existed.
197
+ const dataUrl = await window.NASTECHDesktop.readFileDataUrl(filePath)
198
+ const [, metadata = '', data = ''] = dataUrl.match(/^data:([^,]*),(.*)$/) || []
199
+ const base64 = metadata.includes(';base64')
200
+ const mimeType = metadata.replace(/;base64$/, '') || undefined
201
+ const raw = base64 ? atob(data) : decodeURIComponent(data)
202
+ const bytes = Uint8Array.from(raw, ch => ch.charCodeAt(0))
203
+
204
+ return {
205
+ binary: looksBinaryBytes(bytes),
206
+ byteSize: bytes.byteLength,
207
+ mimeType,
208
+ path: filePath,
209
+ text: new TextDecoder().decode(bytes)
210
+ }
211
+ }
212
+
213
+ // Lightweight markdown renderer for file previews. Streamdown does the parse;
214
+ // our components keep typography simple and route fenced code through Shiki
215
+ // without the library's copy/download/fullscreen chrome.
216
+ const MD_TAG_CLASSES = {
217
+ h1: 'mb-3 mt-6 text-3xl font-bold leading-tight tracking-tight first:mt-0',
218
+ h2: 'mb-2.5 mt-5 text-2xl font-semibold leading-snug tracking-tight first:mt-0',
219
+ h3: 'mb-2 mt-4 text-xl font-semibold leading-snug first:mt-0',
220
+ h4: 'mb-2 mt-3 text-base font-semibold leading-snug first:mt-0',
221
+ p: 'mb-4 leading-relaxed text-foreground last:mb-0',
222
+ ul: 'mb-4 list-disc pl-6 marker:text-muted-foreground/70 last:mb-0',
223
+ ol: 'mb-4 list-decimal pl-6 marker:text-muted-foreground/70 last:mb-0',
224
+ li: 'mt-1 leading-relaxed',
225
+ blockquote: 'mb-4 border-l-2 border-border pl-3 text-muted-foreground italic last:mb-0',
226
+ pre: 'mb-4 overflow-hidden rounded-lg border border-border bg-card font-mono text-xs leading-relaxed last:mb-0 [&_pre]:m-0 [&_pre]:overflow-x-auto [&_pre]:bg-transparent! [&_pre]:p-3 [&_pre]:font-mono'
227
+ } as const
228
+
229
+ function tagged<T extends keyof typeof MD_TAG_CLASSES>(Tag: T) {
230
+ const base = MD_TAG_CLASSES[Tag]
231
+
232
+ const Component = (({ className, ...rest }: ComponentProps<T>) => {
233
+ const Element = Tag as React.ElementType
234
+
235
+ return <Element className={cn(base, className)} {...rest} />
236
+ }) as React.FC<ComponentProps<T>>
237
+
238
+ Component.displayName = `Md.${Tag}`
239
+
240
+ return Component
241
+ }
242
+
243
+ function MarkdownCode({ className, children, ...props }: ComponentProps<'code'>) {
244
+ const language = /language-([^\s]+)/.exec(className || '')?.[1]
245
+
246
+ if (!language) {
247
+ return (
248
+ <code
249
+ className={cn(
250
+ 'rounded bg-muted px-1 py-0.5 font-mono text-[0.86em] text-pink-700 dark:text-pink-300',
251
+ className
252
+ )}
253
+ {...props}
254
+ >
255
+ {children}
256
+ </code>
257
+ )
258
+ }
259
+
260
+ return (
261
+ <ShikiHighlighter
262
+ addDefaultStyles={false}
263
+ as="div"
264
+ defaultColor="light-dark()"
265
+ delay={80}
266
+ language={language}
267
+ showLanguage={false}
268
+ theme={SHIKI_THEME}
269
+ >
270
+ {String(children).replace(/\n$/, '')}
271
+ </ShikiHighlighter>
272
+ )
273
+ }
274
+
275
+ const MARKDOWN_COMPONENTS = {
276
+ h1: tagged('h1'),
277
+ h2: tagged('h2'),
278
+ h3: tagged('h3'),
279
+ h4: tagged('h4'),
280
+ p: tagged('p'),
281
+ ul: tagged('ul'),
282
+ ol: tagged('ol'),
283
+ li: tagged('li'),
284
+ blockquote: tagged('blockquote'),
285
+ pre: tagged('pre'),
286
+ code: MarkdownCode
287
+ }
288
+
289
+ function MarkdownPreview({ text }: { text: string }) {
290
+ return (
291
+ <div className="preview-markdown mx-auto max-w-3xl px-4 py-3 text-sm text-foreground">
292
+ <Streamdown components={MARKDOWN_COMPONENTS} controls={false} mode="static" parseIncompleteMarkdown={false}>
293
+ {text}
294
+ </Streamdown>
295
+ </div>
296
+ )
297
+ }
298
+
299
+ function PreviewToggle({ asSource, onToggle }: { asSource: boolean; onToggle: () => void }) {
300
+ const { t } = useI18n()
301
+
302
+ return (
303
+ <div className="sticky top-0 z-10 flex justify-end border-b border-border/40 bg-transparent px-3 py-1 backdrop-blur">
304
+ <button
305
+ className="text-[0.625rem] font-bold text-muted-foreground underline decoration-current/20 underline-offset-4 transition-colors hover:text-foreground"
306
+ onClick={onToggle}
307
+ type="button"
308
+ >
309
+ {asSource ? t.preview.renderedPreview : t.preview.source}
310
+ </button>
311
+ </div>
312
+ )
313
+ }
314
+
315
+ // Gutter and Shiki output share `font-mono text-xs leading-relaxed py-3` so
316
+ // each line aligns vertically. The selection overlay relies on the same
317
+ // `text-xs * leading-relaxed = 1.21875rem` line-height to position itself.
318
+ const SOURCE_LINE_HEIGHT_REM = 1.21875
319
+ const SOURCE_PAD_Y_REM = 0.75
320
+
321
+ interface LineSelection {
322
+ end: number
323
+ start: number
324
+ }
325
+
326
+ function startLineDrag(event: ReactDragEvent<HTMLElement>, filePath: string, { end, start }: LineSelection) {
327
+ const lineEnd = end > start ? end : undefined
328
+ const label = lineEnd ? `${filePath}:${start}-${end}` : `${filePath}:${start}`
329
+
330
+ event.dataTransfer.setData(NASTECH_PATHS_MIME, JSON.stringify([{ line: start, lineEnd, path: filePath }]))
331
+ event.dataTransfer.setData('text/plain', label)
332
+ event.dataTransfer.effectAllowed = 'copy'
333
+ }
334
+
335
+ function SourceView({ filePath, language, text }: { filePath: string; language: string; text: string }) {
336
+ const { t } = useI18n()
337
+ const lineCount = useMemo(() => Math.max(1, text.split('\n').length), [text])
338
+ const [selection, setSelection] = useState<LineSelection | null>(null)
339
+ const inSelection = (line: number) => selection != null && line >= selection.start && line <= selection.end
340
+
341
+ const handleLineClick = (event: ReactMouseEvent, line: number) => {
342
+ if (event.shiftKey && selection) {
343
+ setSelection({ end: Math.max(selection.end, line), start: Math.min(selection.start, line) })
344
+
345
+ return
346
+ }
347
+
348
+ if (selection?.start === line && selection.end === line) {
349
+ setSelection(null)
350
+
351
+ return
352
+ }
353
+
354
+ setSelection({ end: line, start: line })
355
+ }
356
+
357
+ const handleDragStart = (event: ReactDragEvent<HTMLElement>, line: number) => {
358
+ startLineDrag(event, filePath, inSelection(line) && selection ? selection : { end: line, start: line })
359
+ }
360
+
361
+ return (
362
+ <div className="grid min-w-max grid-cols-[auto_minmax(0,1fr)] font-mono text-xs leading-relaxed">
363
+ <div className="select-none py-3 text-right text-muted-foreground/55">
364
+ {Array.from({ length: lineCount }, (_, index) => {
365
+ const line = index + 1
366
+ const selected = inSelection(line)
367
+
368
+ return (
369
+ <div
370
+ className={cn(
371
+ 'cursor-pointer px-3 tabular-nums transition-colors',
372
+ selected
373
+ ? 'bg-amber-200/45 text-amber-900 dark:bg-amber-300/20 dark:text-amber-100'
374
+ : 'hover:text-foreground'
375
+ )}
376
+ draggable
377
+ key={line}
378
+ onClick={event => handleLineClick(event, line)}
379
+ onDragStart={event => handleDragStart(event, line)}
380
+ title={t.preview.sourceLineTitle}
381
+ >
382
+ {line}
383
+ </div>
384
+ )
385
+ })}
386
+ </div>
387
+ <div className="relative [&_pre]:m-0 [&_pre]:px-3 [&_pre]:py-3 [&_pre]:bg-transparent!">
388
+ {selection && (
389
+ <div
390
+ aria-hidden
391
+ className="pointer-events-none absolute inset-x-0 bg-amber-200/35 dark:bg-amber-300/10"
392
+ style={{
393
+ top: `calc(${SOURCE_PAD_Y_REM}rem + ${selection.start - 1} * ${SOURCE_LINE_HEIGHT_REM}rem)`,
394
+ height: `calc(${selection.end - selection.start + 1} * ${SOURCE_LINE_HEIGHT_REM}rem)`
395
+ }}
396
+ />
397
+ )}
398
+ <ShikiHighlighter
399
+ addDefaultStyles={false}
400
+ as="div"
401
+ defaultColor="light-dark()"
402
+ delay={80}
403
+ language={language || 'text'}
404
+ showLanguage={false}
405
+ theme={SHIKI_THEME}
406
+ >
407
+ {text}
408
+ </ShikiHighlighter>
409
+ </div>
410
+ </div>
411
+ )
412
+ }
413
+
414
+ export function LocalFilePreview({ reloadKey, target }: { reloadKey: number; target: PreviewTarget }) {
415
+ const { t } = useI18n()
416
+ const [state, setState] = useState<LocalPreviewState>({ loading: true })
417
+ const [forcePreview, setForcePreview] = useState(false)
418
+ const [renderMarkdownAsSource, setRenderMarkdownAsSource] = useState(false)
419
+ const filePath = filePathForTarget(target)
420
+ const isImage = target.previewKind === 'image'
421
+
422
+ // HTML files are rendered as source code, not in a webview - so they take
423
+ // the same path as plain text files. `previewKind === 'binary'` arrives
424
+ // when the file is forcibly previewed past the binary refusal screen.
425
+ const isText = target.previewKind === 'text' || target.previewKind === 'binary' || target.previewKind === 'html'
426
+
427
+ const blockedByTarget = !isImage && !forcePreview && (target.binary || target.large)
428
+
429
+ useEffect(() => {
430
+ let active = true
431
+
432
+ async function load() {
433
+ if (blockedByTarget) {
434
+ setState({ loading: false })
435
+
436
+ return
437
+ }
438
+
439
+ if (!isImage && !isText) {
440
+ setState({ loading: false })
441
+
442
+ return
443
+ }
444
+
445
+ setState({ loading: true })
446
+
447
+ try {
448
+ if (isImage) {
449
+ const dataUrl = await window.NASTECHDesktop.readFileDataUrl(filePath)
450
+
451
+ if (active) {
452
+ setState({ dataUrl, loading: false })
453
+ }
454
+
455
+ return
456
+ }
457
+
458
+ const result = await readTextPreview(filePath)
459
+
460
+ if (active) {
461
+ const shouldBlock = !forcePreview && (result.binary || (result.byteSize ?? 0) > TEXT_PREVIEW_MAX_BYTES)
462
+
463
+ setState({
464
+ binary: result.binary,
465
+ byteSize: result.byteSize,
466
+ language: result.language || target.language || 'text',
467
+ loading: false,
468
+ text: shouldBlock ? undefined : result.text,
469
+ truncated: result.truncated
470
+ })
471
+ }
472
+ } catch (error) {
473
+ if (active) {
474
+ setState({
475
+ error: error instanceof Error ? error.message : String(error),
476
+ loading: false
477
+ })
478
+ }
479
+ }
480
+ }
481
+
482
+ void load()
483
+
484
+ return () => {
485
+ active = false
486
+ }
487
+ }, [blockedByTarget, filePath, forcePreview, isImage, isText, reloadKey, target.language])
488
+
489
+ if (state.loading) {
490
+ return <PageLoader label={t.preview.loading} />
491
+ }
492
+
493
+ if (state.error) {
494
+ return <PreviewEmptyState body={state.error} title={t.preview.unavailable} />
495
+ }
496
+
497
+ if (
498
+ !isImage &&
499
+ !forcePreview &&
500
+ (target.binary || target.large || state.binary || (state.byteSize ?? 0) > TEXT_PREVIEW_MAX_BYTES)
501
+ ) {
502
+ const binary = target.binary || state.binary
503
+ const size = target.byteSize || state.byteSize
504
+
505
+ return (
506
+ <PreviewEmptyState
507
+ body={
508
+ binary
509
+ ? t.preview.binaryBody(target.label)
510
+ : t.preview.largeBody(target.label, formatBytes(size))
511
+ }
512
+ primaryAction={{ label: t.preview.previewAnyway, onClick: () => setForcePreview(true) }}
513
+ title={binary ? t.preview.binaryTitle : t.preview.largeTitle}
514
+ tone="warning"
515
+ />
516
+ )
517
+ }
518
+
519
+ if (isImage && state.dataUrl) {
520
+ return (
521
+ <div className="flex h-full w-full items-center justify-center overflow-auto bg-transparent p-4">
522
+ <img
523
+ alt={target.label}
524
+ className="max-h-full max-w-full rounded-lg object-contain shadow-sm"
525
+ draggable={false}
526
+ src={state.dataUrl}
527
+ />
528
+ </div>
529
+ )
530
+ }
531
+
532
+ if (isText && state.text !== undefined) {
533
+ const isMarkdown = (state.language || target.language) === 'markdown'
534
+ const showRendered = isMarkdown && !renderMarkdownAsSource
535
+
536
+ return (
537
+ <div className="h-full overflow-auto bg-transparent">
538
+ {state.truncated && (
539
+ <div className="border-b border-border/60 bg-muted/35 px-3 py-1.5 text-[0.68rem] text-muted-foreground">
540
+ {t.preview.truncated}
541
+ </div>
542
+ )}
543
+ {isMarkdown && <PreviewToggle asSource={!showRendered} onToggle={() => setRenderMarkdownAsSource(s => !s)} />}
544
+ {showRendered ? (
545
+ <MarkdownPreview text={state.text} />
546
+ ) : (
547
+ <SourceView filePath={filePath} language={state.language || 'text'} text={state.text} />
548
+ )}
549
+ </div>
550
+ )
551
+ }
552
+
553
+ return (
554
+ <PreviewEmptyState
555
+ body={t.preview.noInlineBody(target.mimeType || '')}
556
+ title={t.preview.noInlineTitle}
557
+ />
558
+ )
559
+ }
@@ -0,0 +1,43 @@
1
+ import { act, cleanup, render } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { PreviewPane } from './preview-pane'
5
+
6
+ describe('PreviewPane console state', () => {
7
+ afterEach(() => {
8
+ cleanup()
9
+ })
10
+
11
+ it('does not rebuild the pane titlebar group for streamed console logs', () => {
12
+ const setTitlebarToolGroup = vi.fn()
13
+
14
+ const rendered = render(
15
+ <PreviewPane
16
+ setTitlebarToolGroup={setTitlebarToolGroup}
17
+ target={{
18
+ kind: 'url',
19
+ label: 'Preview',
20
+ source: 'http://localhost:5174',
21
+ url: 'http://localhost:5174'
22
+ }}
23
+ />
24
+ )
25
+
26
+ const initialCalls = setTitlebarToolGroup.mock.calls.length
27
+ const webview = rendered.container.querySelector('webview')
28
+
29
+ expect(webview).toBeInstanceOf(HTMLElement)
30
+
31
+ act(() => {
32
+ webview?.dispatchEvent(
33
+ Object.assign(new Event('console-message'), {
34
+ level: 0,
35
+ message: 'streamed log line',
36
+ sourceId: 'http://localhost:5174/src/main.tsx'
37
+ })
38
+ )
39
+ })
40
+
41
+ expect(setTitlebarToolGroup).toHaveBeenCalledTimes(initialCalls)
42
+ })
43
+ })