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,54 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+ const { resolveRequestedPathForIpc } = require('./hardening.cjs')
6
+
7
+ function findGitRoot(start, fsImpl = fs) {
8
+ let dir = start
9
+
10
+ for (let i = 0; i < 50; i += 1) {
11
+ try {
12
+ if (fsImpl.existsSync(path.join(dir, '.git'))) {
13
+ return dir
14
+ }
15
+ } catch {
16
+ return null
17
+ }
18
+
19
+ const parent = path.dirname(dir)
20
+
21
+ if (parent === dir) {
22
+ return null
23
+ }
24
+
25
+ dir = parent
26
+ }
27
+
28
+ return null
29
+ }
30
+
31
+ async function gitRootForIpc(startPath, options = {}) {
32
+ const fsImpl = options.fs || fs
33
+ let resolved
34
+
35
+ try {
36
+ resolved = resolveRequestedPathForIpc(startPath, { purpose: 'Git root' })
37
+ } catch {
38
+ return null
39
+ }
40
+
41
+ try {
42
+ const stat = await fsImpl.promises.stat(resolved)
43
+ const start = stat.isDirectory() ? resolved : path.dirname(resolved)
44
+
45
+ return findGitRoot(start, fsImpl)
46
+ } catch {
47
+ return findGitRoot(resolved, fsImpl)
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ findGitRoot,
53
+ gitRootForIpc
54
+ }
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert/strict')
4
+ const fs = require('node:fs')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+ const test = require('node:test')
8
+ const { pathToFileURL } = require('node:url')
9
+
10
+ const { gitRootForIpc } = require('./git-root.cjs')
11
+
12
+ function mkTmpDir() {
13
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-git-root-'))
14
+ }
15
+
16
+ test('gitRootForIpc returns null for invalid and device paths', async () => {
17
+ assert.equal(await gitRootForIpc(''), null)
18
+ assert.equal(await gitRootForIpc(' '), null)
19
+ assert.equal(await gitRootForIpc(null), null)
20
+ assert.equal(await gitRootForIpc('\\\\?\\C:\\secret'), null)
21
+ assert.equal(await gitRootForIpc('file:///%E0%A4%A'), null)
22
+ })
23
+
24
+ test('gitRootForIpc resolves directories files missing descendants and file URLs', async t => {
25
+ const root = mkTmpDir()
26
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }))
27
+
28
+ const gitDir = path.join(root, '.git')
29
+ const srcDir = path.join(root, 'src')
30
+ const filePath = path.join(srcDir, 'index.ts')
31
+ fs.mkdirSync(gitDir)
32
+ fs.mkdirSync(srcDir)
33
+ fs.writeFileSync(filePath, 'export {}\n', 'utf8')
34
+
35
+ assert.equal(await gitRootForIpc(root), root)
36
+ assert.equal(await gitRootForIpc(srcDir), root)
37
+ assert.equal(await gitRootForIpc(filePath), root)
38
+ assert.equal(await gitRootForIpc(pathToFileURL(filePath).toString()), root)
39
+ assert.equal(await gitRootForIpc(path.join(srcDir, 'missing.ts')), root)
40
+ })
@@ -0,0 +1,174 @@
1
+ 'use strict'
2
+
3
+ // Resolve git-worktree relationships for a set of session cwds, reading git's
4
+ // on-disk metadata directly (no `git` spawn per path):
5
+ //
6
+ // - A normal checkout has a `.git` DIRECTORY at its root → it's the main
7
+ // worktree; its repo root IS that directory's parent.
8
+ // - A linked worktree has a `.git` FILE: `gitdir: <repo>/.git/worktrees/<name>`.
9
+ // That admin dir's `commondir` points back at the shared `<repo>/.git`, whose
10
+ // parent is the main repo root.
11
+ //
12
+ // Grouping by repoRoot therefore clusters a repo's main checkout with all of its
13
+ // linked worktrees, regardless of how the worktree directories are named. The
14
+ // branch (read from the worktree's own HEAD) gives each worktree a meaningful
15
+ // label.
16
+
17
+ const fs = require('node:fs')
18
+ const path = require('node:path')
19
+ const { resolveRequestedPathForIpc } = require('./hardening.cjs')
20
+
21
+ // Walk up from `start` to the nearest ancestor that carries a `.git` entry
22
+ // (file for a linked worktree, dir for the main checkout). Capped so a stray
23
+ // path can't loop forever.
24
+ function findGitHost(start, fsImpl) {
25
+ let dir = start
26
+
27
+ for (let i = 0; i < 64; i += 1) {
28
+ const dotgit = path.join(dir, '.git')
29
+
30
+ try {
31
+ if (fsImpl.existsSync(dotgit)) {
32
+ return dir
33
+ }
34
+ } catch {
35
+ return null
36
+ }
37
+
38
+ const parent = path.dirname(dir)
39
+
40
+ if (parent === dir) {
41
+ return null
42
+ }
43
+
44
+ dir = parent
45
+ }
46
+
47
+ return null
48
+ }
49
+
50
+ function readBranch(gitDir, fsImpl) {
51
+ try {
52
+ const head = fsImpl.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim()
53
+ const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/)
54
+
55
+ if (ref) {
56
+ return ref[1]
57
+ }
58
+
59
+ // Detached HEAD: surface a short sha so the worktree still gets a label.
60
+ return /^[0-9a-f]{7,40}$/i.test(head) ? head.slice(0, 8) : null
61
+ } catch {
62
+ return null
63
+ }
64
+ }
65
+
66
+ // Given the directory that owns the `.git` entry, resolve its worktree identity.
67
+ function resolveFromHost(host, fsImpl) {
68
+ const dotgit = path.join(host, '.git')
69
+ let stat
70
+
71
+ try {
72
+ stat = fsImpl.statSync(dotgit)
73
+ } catch {
74
+ return null
75
+ }
76
+
77
+ if (stat.isDirectory()) {
78
+ return {
79
+ repoRoot: host,
80
+ worktreeRoot: host,
81
+ isMainWorktree: true,
82
+ branch: readBranch(dotgit, fsImpl)
83
+ }
84
+ }
85
+
86
+ // Linked worktree: `.git` is a file pointing at the admin dir.
87
+ let contents
88
+
89
+ try {
90
+ contents = fsImpl.readFileSync(dotgit, 'utf8').trim()
91
+ } catch {
92
+ return null
93
+ }
94
+
95
+ const match = contents.match(/^gitdir:\s*(.+)$/m)
96
+
97
+ if (!match) {
98
+ return null
99
+ }
100
+
101
+ const adminDir = path.resolve(host, match[1].trim())
102
+
103
+ // `commondir` resolves to the shared `<repo>/.git`; fall back to walking two
104
+ // levels up from `<repo>/.git/worktrees/<name>` if it's missing.
105
+ let commonDir
106
+
107
+ try {
108
+ const rel = fsImpl.readFileSync(path.join(adminDir, 'commondir'), 'utf8').trim()
109
+ commonDir = path.resolve(adminDir, rel)
110
+ } catch {
111
+ commonDir = path.dirname(path.dirname(adminDir))
112
+ }
113
+
114
+ return {
115
+ repoRoot: path.dirname(commonDir),
116
+ worktreeRoot: host,
117
+ isMainWorktree: false,
118
+ branch: readBranch(adminDir, fsImpl)
119
+ }
120
+ }
121
+
122
+ function resolveWorktree(startPath, fsImpl = fs) {
123
+ let resolved
124
+
125
+ try {
126
+ resolved = resolveRequestedPathForIpc(startPath, { purpose: 'Worktree lookup' })
127
+ } catch {
128
+ return null
129
+ }
130
+
131
+ let start = resolved
132
+
133
+ try {
134
+ const stat = fsImpl.statSync(resolved)
135
+
136
+ if (!stat.isDirectory()) {
137
+ start = path.dirname(resolved)
138
+ }
139
+ } catch {
140
+ return null
141
+ }
142
+
143
+ const host = findGitHost(start, fsImpl)
144
+
145
+ if (!host) {
146
+ return null
147
+ }
148
+
149
+ return resolveFromHost(host, fsImpl)
150
+ }
151
+
152
+ // Batch entry point for the renderer: maps each requested cwd to its worktree
153
+ // info (or null when it isn't inside a git checkout / can't be read). Dedupes so
154
+ // many sessions sharing a cwd cost one lookup.
155
+ async function worktreesForIpc(cwds, options = {}) {
156
+ const fsImpl = options.fs || fs
157
+ const list = Array.isArray(cwds) ? cwds : []
158
+ const out = {}
159
+
160
+ for (const cwd of list) {
161
+ if (typeof cwd !== 'string' || !cwd.trim() || cwd in out) {
162
+ continue
163
+ }
164
+
165
+ out[cwd] = resolveWorktree(cwd, fsImpl)
166
+ }
167
+
168
+ return out
169
+ }
170
+
171
+ module.exports = {
172
+ resolveWorktree,
173
+ worktreesForIpc
174
+ }
@@ -0,0 +1,184 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+ const { fileURLToPath } = require('node:url')
4
+
5
+ const DEFAULT_FETCH_TIMEOUT_MS = 15_000
6
+ const DATA_URL_READ_MAX_BYTES = 16 * 1024 * 1024
7
+ const TEXT_PREVIEW_SOURCE_MAX_BYTES = 64 * 1024 * 1024
8
+
9
+ const SAFE_ENV_SUFFIXES = new Set(['dist', 'example', 'sample', 'template'])
10
+ const SENSITIVE_EXTENSIONS = new Set(['.kdbx', '.p12', '.pem', '.pfx'])
11
+
12
+ function resolveTimeoutMs(timeoutMs, fallbackMs = DEFAULT_FETCH_TIMEOUT_MS) {
13
+ const fallback =
14
+ Number.isFinite(fallbackMs) && Number(fallbackMs) > 0 ? Math.round(Number(fallbackMs)) : DEFAULT_FETCH_TIMEOUT_MS
15
+ const parsed = Number(timeoutMs)
16
+
17
+ if (Number.isFinite(parsed) && parsed > 0) {
18
+ return Math.round(parsed)
19
+ }
20
+
21
+ return fallback
22
+ }
23
+
24
+ function encryptDesktopSecret(value, safeStorageApi) {
25
+ const raw = String(value || '')
26
+
27
+ if (!raw) {
28
+ return null
29
+ }
30
+
31
+ let encryptionAvailable = false
32
+
33
+ try {
34
+ encryptionAvailable = Boolean(safeStorageApi?.isEncryptionAvailable?.())
35
+ } catch {
36
+ encryptionAvailable = false
37
+ }
38
+
39
+ if (!encryptionAvailable) {
40
+ throw new Error(
41
+ 'Secure token storage is unavailable, so NasTech Desktop cannot save remote gateway tokens. ' +
42
+ 'Set NASTECH_DESKTOP_REMOTE_URL and NASTECH_DESKTOP_REMOTE_TOKEN in your environment, or enable OS keychain access and try again.'
43
+ )
44
+ }
45
+
46
+ try {
47
+ return {
48
+ encoding: 'safeStorage',
49
+ value: safeStorageApi.encryptString(raw).toString('base64')
50
+ }
51
+ } catch (error) {
52
+ const detail = error instanceof Error && error.message ? ` (${error.message})` : ''
53
+ throw new Error(
54
+ `Failed to encrypt the remote gateway token for secure storage${detail}. ` +
55
+ 'Set NASTECH_DESKTOP_REMOTE_URL and NASTECH_DESKTOP_REMOTE_TOKEN in your environment as a fallback.'
56
+ )
57
+ }
58
+ }
59
+
60
+ function sensitiveFileBlockReason(filePath) {
61
+ const normalized = String(filePath || '')
62
+ .replace(/\\/g, '/')
63
+ .toLowerCase()
64
+ const basename = path.basename(normalized)
65
+ const ext = path.extname(basename)
66
+
67
+ if (!basename) {
68
+ return null
69
+ }
70
+
71
+ if (normalized.includes('/.ssh/')) {
72
+ return 'SSH key/config files are blocked.'
73
+ }
74
+
75
+ if (normalized.includes('/.gnupg/')) {
76
+ return 'GPG key material is blocked.'
77
+ }
78
+
79
+ if (normalized.endsWith('/.aws/credentials')) {
80
+ return 'AWS credential files are blocked.'
81
+ }
82
+
83
+ if (basename === '.env') {
84
+ return '.env files are blocked because they commonly contain secrets.'
85
+ }
86
+
87
+ if (basename.startsWith('.env.')) {
88
+ const suffix = basename.slice('.env.'.length)
89
+ if (!SAFE_ENV_SUFFIXES.has(suffix)) {
90
+ return `${basename} is blocked because it appears to contain environment secrets.`
91
+ }
92
+ }
93
+
94
+ if (/^id_(rsa|dsa|ecdsa|ed25519)(?:\..+)?$/.test(basename) && !basename.endsWith('.pub')) {
95
+ return 'SSH private key files are blocked.'
96
+ }
97
+
98
+ if (SENSITIVE_EXTENSIONS.has(ext)) {
99
+ return `${ext} key/certificate files are blocked.`
100
+ }
101
+
102
+ if (basename === '.npmrc' || basename === '.netrc' || basename === '.pypirc') {
103
+ return `${basename} is blocked because it may include auth credentials.`
104
+ }
105
+
106
+ return null
107
+ }
108
+
109
+ function resolveRequestedFilePath(filePath, baseDir = process.cwd(), purpose = 'File read') {
110
+ const raw = String(filePath || '').trim()
111
+
112
+ if (!raw) {
113
+ throw new Error(`${purpose} failed: file path is required.`)
114
+ }
115
+
116
+ if (raw.includes('\0')) {
117
+ throw new Error(`${purpose} failed: file path is invalid.`)
118
+ }
119
+
120
+ if (/^file:/i.test(raw)) {
121
+ try {
122
+ return fileURLToPath(raw)
123
+ } catch {
124
+ throw new Error(`${purpose} failed: file URL is invalid.`)
125
+ }
126
+ }
127
+
128
+ const resolvedBase = path.resolve(String(baseDir || process.cwd()))
129
+ return path.resolve(resolvedBase, raw)
130
+ }
131
+
132
+ async function resolveReadableFileForIpc(filePath, options = {}) {
133
+ const purpose = String(options.purpose || 'File read')
134
+ const resolvedPath = resolveRequestedFilePath(filePath, options.baseDir, purpose)
135
+
136
+ if (options.blockSensitive !== false) {
137
+ const blockReason = sensitiveFileBlockReason(resolvedPath)
138
+ if (blockReason) {
139
+ throw new Error(`${purpose} blocked for sensitive file: ${blockReason}`)
140
+ }
141
+ }
142
+
143
+ let stat
144
+ try {
145
+ stat = await fs.promises.stat(resolvedPath)
146
+ } catch (error) {
147
+ const code = error && typeof error === 'object' ? error.code : ''
148
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
149
+ throw new Error(`${purpose} failed: file does not exist.`)
150
+ }
151
+ throw new Error(`${purpose} failed: ${error instanceof Error ? error.message : String(error)}`)
152
+ }
153
+
154
+ if (stat.isDirectory()) {
155
+ throw new Error(`${purpose} failed: path points to a directory.`)
156
+ }
157
+
158
+ if (!stat.isFile()) {
159
+ throw new Error(`${purpose} failed: only regular files can be read.`)
160
+ }
161
+
162
+ const maxBytes = Number.isFinite(options.maxBytes) && Number(options.maxBytes) > 0 ? Number(options.maxBytes) : null
163
+ if (maxBytes && stat.size > maxBytes) {
164
+ throw new Error(`${purpose} failed: file is too large (${stat.size} bytes; limit ${maxBytes} bytes).`)
165
+ }
166
+
167
+ try {
168
+ await fs.promises.access(resolvedPath, fs.constants.R_OK)
169
+ } catch {
170
+ throw new Error(`${purpose} failed: file is not readable.`)
171
+ }
172
+
173
+ return { resolvedPath, stat }
174
+ }
175
+
176
+ module.exports = {
177
+ DATA_URL_READ_MAX_BYTES,
178
+ DEFAULT_FETCH_TIMEOUT_MS,
179
+ TEXT_PREVIEW_SOURCE_MAX_BYTES,
180
+ encryptDesktopSecret,
181
+ resolveReadableFileForIpc,
182
+ resolveTimeoutMs,
183
+ sensitiveFileBlockReason
184
+ }
@@ -0,0 +1,116 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const test = require('node:test')
6
+ const { pathToFileURL } = require('node:url')
7
+
8
+ const {
9
+ DEFAULT_FETCH_TIMEOUT_MS,
10
+ encryptDesktopSecret,
11
+ resolveReadableFileForIpc,
12
+ resolveTimeoutMs,
13
+ sensitiveFileBlockReason
14
+ } = require('./hardening.cjs')
15
+
16
+ test('resolveTimeoutMs falls back to defaults and accepts overrides', () => {
17
+ assert.equal(resolveTimeoutMs(undefined), DEFAULT_FETCH_TIMEOUT_MS)
18
+ assert.equal(resolveTimeoutMs(0), DEFAULT_FETCH_TIMEOUT_MS)
19
+ assert.equal(resolveTimeoutMs(-25), DEFAULT_FETCH_TIMEOUT_MS)
20
+ assert.equal(resolveTimeoutMs('2750'), 2750)
21
+ })
22
+
23
+ test('encryptDesktopSecret requires available secure storage', () => {
24
+ assert.equal(
25
+ encryptDesktopSecret('', { isEncryptionAvailable: () => true, encryptString: () => Buffer.alloc(0) }),
26
+ null
27
+ )
28
+
29
+ assert.throws(
30
+ () => encryptDesktopSecret('token', { isEncryptionAvailable: () => false, encryptString: () => Buffer.alloc(0) }),
31
+ /Secure token storage is unavailable/
32
+ )
33
+ })
34
+
35
+ test('encryptDesktopSecret stores safeStorage base64 payload', () => {
36
+ const secret = encryptDesktopSecret('token-123', {
37
+ isEncryptionAvailable: () => true,
38
+ encryptString: value => Buffer.from(`enc:${value}`, 'utf8')
39
+ })
40
+
41
+ assert.deepEqual(secret, {
42
+ encoding: 'safeStorage',
43
+ value: Buffer.from('enc:token-123', 'utf8').toString('base64')
44
+ })
45
+ })
46
+
47
+ test('sensitiveFileBlockReason blocks obvious secret file patterns', () => {
48
+ assert.match(String(sensitiveFileBlockReason('/tmp/.env')), /\.env/)
49
+ assert.equal(sensitiveFileBlockReason('/tmp/.env.example'), null)
50
+ assert.match(String(sensitiveFileBlockReason('/Users/me/.ssh/id_ed25519')), /SSH/)
51
+ assert.match(String(sensitiveFileBlockReason('/tmp/server-cert.pem')), /\.pem/)
52
+ })
53
+
54
+ test('resolveReadableFileForIpc validates existence type size and sensitivity', async t => {
55
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-desktop-hardening-'))
56
+ t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
57
+
58
+ const textPath = path.join(tempDir, 'notes.txt')
59
+ fs.writeFileSync(textPath, 'hello world', 'utf8')
60
+
61
+ const fromRelative = await resolveReadableFileForIpc('notes.txt', {
62
+ baseDir: tempDir,
63
+ maxBytes: 256,
64
+ purpose: 'File preview'
65
+ })
66
+ assert.equal(fromRelative.resolvedPath, textPath)
67
+ assert.equal(fromRelative.stat.size, 11)
68
+
69
+ const fromFileUrl = await resolveReadableFileForIpc(pathToFileURL(textPath).toString(), {
70
+ purpose: 'File preview'
71
+ })
72
+ assert.equal(fromFileUrl.resolvedPath, textPath)
73
+
74
+ await assert.rejects(
75
+ resolveReadableFileForIpc('missing.txt', {
76
+ baseDir: tempDir,
77
+ purpose: 'Text preview'
78
+ }),
79
+ /file does not exist/
80
+ )
81
+
82
+ const nestedDir = path.join(tempDir, 'directory')
83
+ fs.mkdirSync(nestedDir)
84
+ await assert.rejects(
85
+ resolveReadableFileForIpc(nestedDir, {
86
+ purpose: 'Text preview'
87
+ }),
88
+ /path points to a directory/
89
+ )
90
+
91
+ const largePath = path.join(tempDir, 'large.txt')
92
+ fs.writeFileSync(largePath, 'x'.repeat(40), 'utf8')
93
+ await assert.rejects(
94
+ resolveReadableFileForIpc(largePath, {
95
+ maxBytes: 8,
96
+ purpose: 'File preview'
97
+ }),
98
+ /file is too large/
99
+ )
100
+
101
+ const envPath = path.join(tempDir, '.env')
102
+ fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8')
103
+ await assert.rejects(
104
+ resolveReadableFileForIpc(envPath, {
105
+ purpose: 'File preview'
106
+ }),
107
+ /blocked for sensitive file/
108
+ )
109
+
110
+ const envTemplatePath = path.join(tempDir, '.env.example')
111
+ fs.writeFileSync(envTemplatePath, 'EXAMPLE_TOKEN=value', 'utf8')
112
+ const envTemplate = await resolveReadableFileForIpc(envTemplatePath, {
113
+ purpose: 'File preview'
114
+ })
115
+ assert.equal(envTemplate.resolvedPath, envTemplatePath)
116
+ })