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.
- package/.prettierrc +11 -0
- package/DESIGN.md +167 -0
- package/README.md +141 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/components.json +21 -0
- package/electron/backend-env.cjs +112 -0
- package/electron/backend-env.test.cjs +111 -0
- package/electron/backend-probes.cjs +106 -0
- package/electron/backend-probes.test.cjs +82 -0
- package/electron/backend-ready.cjs +66 -0
- package/electron/bootstrap-platform.cjs +91 -0
- package/electron/bootstrap-platform.test.cjs +111 -0
- package/electron/bootstrap-runner.cjs +720 -0
- package/electron/bootstrap-runner.test.cjs +138 -0
- package/electron/connection-config.cjs +254 -0
- package/electron/connection-config.test.cjs +329 -0
- package/electron/dashboard-token.cjs +99 -0
- package/electron/dashboard-token.test.cjs +142 -0
- package/electron/desktop-uninstall.cjs +232 -0
- package/electron/desktop-uninstall.test.cjs +246 -0
- package/electron/entitlements.mac.inherit.plist +14 -0
- package/electron/entitlements.mac.plist +14 -0
- package/electron/fs-read-dir.cjs +109 -0
- package/electron/fs-read-dir.test.cjs +364 -0
- package/electron/gateway-ws-probe.cjs +188 -0
- package/electron/gateway-ws-probe.test.cjs +122 -0
- package/electron/git-root.cjs +54 -0
- package/electron/git-root.test.cjs +40 -0
- package/electron/git-worktrees.cjs +174 -0
- package/electron/hardening.cjs +184 -0
- package/electron/hardening.test.cjs +116 -0
- package/electron/main.cjs +5762 -0
- package/electron/oauth-net-request.cjs +20 -0
- package/electron/oauth-net-request.test.cjs +34 -0
- package/electron/preload.cjs +135 -0
- package/electron/session-windows.cjs +99 -0
- package/electron/session-windows.test.cjs +177 -0
- package/electron/update-remote.cjs +56 -0
- package/electron/update-remote.test.cjs +78 -0
- package/electron/vscode-marketplace.cjs +331 -0
- package/electron/vscode-marketplace.test.cjs +113 -0
- package/electron/windows-child-process.test.cjs +57 -0
- package/electron/windows-user-env.cjs +76 -0
- package/electron/windows-user-env.test.cjs +90 -0
- package/electron/workspace-cwd.cjs +38 -0
- package/electron/workspace-cwd.test.cjs +45 -0
- package/eslint.config.mjs +122 -0
- package/index.html +17 -0
- package/package.json +254 -0
- package/pr-assets/session-source-folders.png +0 -0
- package/preview-demo.html +65 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/ds-assets/filler-bg0.jpg +0 -0
- package/public/nastech-frames/nastech-frame-0.png +0 -0
- package/public/nastech-frames/nastech-frame-1.png +0 -0
- package/public/nastech-frames/nastech-frame-2.png +0 -0
- package/public/nastech-frames/nastech-frame-3.png +0 -0
- package/public/nastech-frames/nastech-frame-4.png +0 -0
- package/public/nastech-frames/nastech-frame-5.png +0 -0
- package/public/nastech-frames/nastech-frame-6.png +0 -0
- package/public/nastech-frames/nastech-frame-7.png +0 -0
- package/public/nastech-girl.jpg +0 -0
- package/public/nastech-sprite.png +0 -0
- package/public/nastech.png +0 -0
- package/scripts/after-pack.cjs +41 -0
- package/scripts/assert-dist-built.cjs +70 -0
- package/scripts/assert-dist-built.test.cjs +84 -0
- package/scripts/assert-root-install.cjs +13 -0
- package/scripts/before-build.cjs +11 -0
- package/scripts/before-pack.cjs +78 -0
- package/scripts/before-pack.test.cjs +53 -0
- package/scripts/click-session.mjs +51 -0
- package/scripts/dev-no-hmr.mjs +22 -0
- package/scripts/diag-jump.mjs +115 -0
- package/scripts/diag-scroll-reset.mjs +229 -0
- package/scripts/eval.mjs +21 -0
- package/scripts/leak-typing.mjs +222 -0
- package/scripts/measure-jump.mjs +108 -0
- package/scripts/measure-latency.mjs +184 -0
- package/scripts/measure-real-stream.mjs +252 -0
- package/scripts/measure-submit.mjs +179 -0
- package/scripts/measure-synthetic-stream.mjs +322 -0
- package/scripts/notarize-artifact.cjs +77 -0
- package/scripts/notarize.cjs +100 -0
- package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
- package/scripts/probe-renderer.mjs +38 -0
- package/scripts/probe-thread.mjs +40 -0
- package/scripts/profile-long-stream.mjs +191 -0
- package/scripts/profile-real-stream.mjs +137 -0
- package/scripts/profile-synth-stream.mjs +103 -0
- package/scripts/profile-typing-lag.md +381 -0
- package/scripts/profile-typing.mjs +260 -0
- package/scripts/reload-renderer.mjs +25 -0
- package/scripts/reload.mjs +36 -0
- package/scripts/set-exe-identity.cjs +94 -0
- package/scripts/stage-native-deps.cjs +159 -0
- package/scripts/test-desktop.mjs +425 -0
- package/scripts/write-build-stamp.cjs +126 -0
- package/src/app/agents/index.tsx +398 -0
- package/src/app/artifacts/index.test.ts +62 -0
- package/src/app/artifacts/index.tsx +906 -0
- package/src/app/chat/chat-drop-overlay.tsx +48 -0
- package/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/src/app/chat/composer/attachments.tsx +114 -0
- package/src/app/chat/composer/completion-drawer.tsx +63 -0
- package/src/app/chat/composer/context-menu.tsx +172 -0
- package/src/app/chat/composer/controls.tsx +289 -0
- package/src/app/chat/composer/drop-affordance.ts +2 -0
- package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/src/app/chat/composer/focus.ts +134 -0
- package/src/app/chat/composer/help-hint.tsx +59 -0
- package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
- package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
- package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
- package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
- package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
- package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
- package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/src/app/chat/composer/index.tsx +1611 -0
- package/src/app/chat/composer/inline-refs.ts +138 -0
- package/src/app/chat/composer/model-pill.tsx +86 -0
- package/src/app/chat/composer/queue-panel.tsx +130 -0
- package/src/app/chat/composer/rich-editor.test.ts +18 -0
- package/src/app/chat/composer/rich-editor.ts +165 -0
- package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
- package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/src/app/chat/composer/text-utils.test.ts +77 -0
- package/src/app/chat/composer/text-utils.ts +107 -0
- package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
- package/src/app/chat/composer/trigger-popover.tsx +116 -0
- package/src/app/chat/composer/types.ts +64 -0
- package/src/app/chat/composer/url-dialog.tsx +82 -0
- package/src/app/chat/composer/voice-activity.tsx +252 -0
- package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/src/app/chat/hooks/use-composer-actions.ts +525 -0
- package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
- package/src/app/chat/index.tsx +390 -0
- package/src/app/chat/perf-probe.tsx +269 -0
- package/src/app/chat/right-rail/index.ts +1 -0
- package/src/app/chat/right-rail/preview-console-state.ts +82 -0
- package/src/app/chat/right-rail/preview-console.tsx +290 -0
- package/src/app/chat/right-rail/preview-file.tsx +559 -0
- package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
- package/src/app/chat/right-rail/preview-pane.tsx +657 -0
- package/src/app/chat/right-rail/preview.tsx +171 -0
- package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
- package/src/app/chat/sidebar/index.tsx +1219 -0
- package/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/src/app/chat/sidebar/order.test.ts +21 -0
- package/src/app/chat/sidebar/order.ts +17 -0
- package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
- package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
- package/src/app/chat/sidebar/session-row.tsx +257 -0
- package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
- package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/src/app/chat/thread-loading.test.ts +34 -0
- package/src/app/chat/thread-loading.ts +26 -0
- package/src/app/command-center/index.tsx +654 -0
- package/src/app/command-palette/index.tsx +513 -0
- package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/src/app/cron/index.tsx +942 -0
- package/src/app/cron/job-state.ts +29 -0
- package/src/app/desktop-controller.tsx +938 -0
- package/src/app/floating-hud.ts +22 -0
- package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
- package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
- package/src/app/hooks/use-keybinds.ts +186 -0
- package/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/src/app/hooks/use-route-enum-param.ts +38 -0
- package/src/app/index.tsx +1 -0
- package/src/app/layout-constants.ts +13 -0
- package/src/app/messaging/index.tsx +648 -0
- package/src/app/messaging/platform-icon.tsx +93 -0
- package/src/app/model-picker-overlay.tsx +42 -0
- package/src/app/model-visibility-overlay.tsx +31 -0
- package/src/app/overlays/overlay-chrome.tsx +66 -0
- package/src/app/overlays/overlay-search-input.tsx +33 -0
- package/src/app/overlays/overlay-split-layout.tsx +130 -0
- package/src/app/overlays/overlay-view.tsx +91 -0
- package/src/app/page-search-shell.tsx +75 -0
- package/src/app/profiles/create-profile-dialog.tsx +154 -0
- package/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/src/app/profiles/index.tsx +671 -0
- package/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/src/app/right-sidebar/files/ipc.ts +161 -0
- package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/src/app/right-sidebar/files/tree.tsx +224 -0
- package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
- package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
- package/src/app/right-sidebar/index.test.tsx +75 -0
- package/src/app/right-sidebar/index.tsx +395 -0
- package/src/app/right-sidebar/store.ts +15 -0
- package/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/src/app/right-sidebar/terminal/index.tsx +98 -0
- package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
- package/src/app/right-sidebar/terminal/selection.ts +75 -0
- package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
- package/src/app/routes.ts +88 -0
- package/src/app/session/hooks/use-context-suggestions.ts +58 -0
- package/src/app/session/hooks/use-cwd-actions.ts +109 -0
- package/src/app/session/hooks/use-message-stream.ts +957 -0
- package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/src/app/session/hooks/use-model-controls.ts +106 -0
- package/src/app/session/hooks/use-nastech-config.ts +74 -0
- package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
- package/src/app/session/hooks/use-preview-routing.ts +223 -0
- package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
- package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
- package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
- package/src/app/session/hooks/use-route-resume.ts +115 -0
- package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
- package/src/app/session/hooks/use-session-actions.ts +885 -0
- package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
- package/src/app/session/hooks/use-session-state-cache.ts +191 -0
- package/src/app/session-picker-overlay.tsx +32 -0
- package/src/app/session-switcher.tsx +107 -0
- package/src/app/settings/about-settings.tsx +173 -0
- package/src/app/settings/appearance-settings.tsx +162 -0
- package/src/app/settings/config-settings.tsx +384 -0
- package/src/app/settings/constants.ts +545 -0
- package/src/app/settings/credential-key-ui.tsx +373 -0
- package/src/app/settings/env-credentials.tsx +198 -0
- package/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/src/app/settings/field-copy.ts +56 -0
- package/src/app/settings/gateway-settings.tsx +620 -0
- package/src/app/settings/helpers.test.ts +138 -0
- package/src/app/settings/helpers.ts +151 -0
- package/src/app/settings/index.tsx +237 -0
- package/src/app/settings/keys-settings.tsx +96 -0
- package/src/app/settings/mcp-settings.tsx +271 -0
- package/src/app/settings/model-settings.test.tsx +157 -0
- package/src/app/settings/model-settings.tsx +559 -0
- package/src/app/settings/notifications-settings.tsx +150 -0
- package/src/app/settings/primitives.tsx +115 -0
- package/src/app/settings/providers-settings.test.tsx +100 -0
- package/src/app/settings/providers-settings.tsx +258 -0
- package/src/app/settings/sessions-settings.tsx +276 -0
- package/src/app/settings/toolset-config-panel.test.tsx +289 -0
- package/src/app/settings/toolset-config-panel.tsx +449 -0
- package/src/app/settings/types.ts +42 -0
- package/src/app/settings/uninstall-section.tsx +185 -0
- package/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/src/app/shell/app-shell.tsx +167 -0
- package/src/app/shell/gateway-menu-panel.tsx +150 -0
- package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
- package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
- package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
- package/src/app/shell/keybind-panel.tsx +220 -0
- package/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/src/app/shell/model-edit-submenu.tsx +245 -0
- package/src/app/shell/model-menu-panel.tsx +295 -0
- package/src/app/shell/sidebar-label.tsx +22 -0
- package/src/app/shell/statusbar-controls.tsx +185 -0
- package/src/app/shell/titlebar-controls.tsx +244 -0
- package/src/app/shell/titlebar.test.ts +26 -0
- package/src/app/shell/titlebar.ts +45 -0
- package/src/app/shell/use-group-registry.ts +39 -0
- package/src/app/skills/index.test.tsx +103 -0
- package/src/app/skills/index.tsx +371 -0
- package/src/app/types.ts +99 -0
- package/src/app/updates-overlay.tsx +369 -0
- package/src/components/Backdrop.tsx +114 -0
- package/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/src/components/assistant-ui/clarify-tool.tsx +281 -0
- package/src/components/assistant-ui/directive-text.test.ts +39 -0
- package/src/components/assistant-ui/directive-text.tsx +389 -0
- package/src/components/assistant-ui/markdown-text.test.ts +204 -0
- package/src/components/assistant-ui/markdown-text.tsx +497 -0
- package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/src/components/assistant-ui/streaming.test.tsx +739 -0
- package/src/components/assistant-ui/thread-list.tsx +307 -0
- package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
- package/src/components/assistant-ui/thread.tsx +1474 -0
- package/src/components/assistant-ui/todo-tool.tsx +109 -0
- package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
- package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
- package/src/components/assistant-ui/tool-approval.tsx +209 -0
- package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
- package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
- package/src/components/assistant-ui/tool-fallback.tsx +466 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
- package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/src/components/assistant-ui/user-message-text.tsx +150 -0
- package/src/components/boot-failure-overlay.tsx +246 -0
- package/src/components/boot-failure-reauth.test.ts +100 -0
- package/src/components/boot-failure-reauth.ts +81 -0
- package/src/components/brand-mark.tsx +19 -0
- package/src/components/chat/activity-timer-text.tsx +24 -0
- package/src/components/chat/activity-timer.test.tsx +43 -0
- package/src/components/chat/activity-timer.ts +64 -0
- package/src/components/chat/code-card.tsx +78 -0
- package/src/components/chat/compact-markdown.tsx +113 -0
- package/src/components/chat/composer-dock.ts +31 -0
- package/src/components/chat/diff-lines.tsx +54 -0
- package/src/components/chat/disclosure-row.tsx +63 -0
- package/src/components/chat/generated-image-context.tsx +19 -0
- package/src/components/chat/generated-image-result.tsx +174 -0
- package/src/components/chat/image-generation-placeholder.tsx +279 -0
- package/src/components/chat/intro-copy.jsonl +75 -0
- package/src/components/chat/intro.tsx +182 -0
- package/src/components/chat/preview-attachment.tsx +125 -0
- package/src/components/chat/shiki-highlighter.tsx +107 -0
- package/src/components/chat/status-row.tsx +70 -0
- package/src/components/chat/status-section.tsx +42 -0
- package/src/components/chat/terminal-output.tsx +50 -0
- package/src/components/chat/zoomable-image.tsx +177 -0
- package/src/components/desktop-install-overlay.tsx +595 -0
- package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
- package/src/components/desktop-onboarding-overlay.tsx +1286 -0
- package/src/components/error-boundary.tsx +77 -0
- package/src/components/gateway-connecting-overlay.test.tsx +143 -0
- package/src/components/gateway-connecting-overlay.tsx +183 -0
- package/src/components/haptics-provider.tsx +19 -0
- package/src/components/language-switcher.test.tsx +53 -0
- package/src/components/language-switcher.tsx +175 -0
- package/src/components/model-picker.tsx +340 -0
- package/src/components/model-visibility-dialog.tsx +155 -0
- package/src/components/notifications.tsx +196 -0
- package/src/components/page-loader.tsx +34 -0
- package/src/components/pane-shell/context.ts +14 -0
- package/src/components/pane-shell/index.ts +4 -0
- package/src/components/pane-shell/pane-shell.test.tsx +333 -0
- package/src/components/pane-shell/pane-shell.tsx +330 -0
- package/src/components/prompt-overlays.tsx +234 -0
- package/src/components/session-picker.tsx +108 -0
- package/src/components/status-dot.tsx +26 -0
- package/src/components/ui/action-status.tsx +25 -0
- package/src/components/ui/alert.tsx +53 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/braille-spinner.tsx +61 -0
- package/src/components/ui/button.tsx +81 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/codicon.tsx +20 -0
- package/src/components/ui/command.tsx +111 -0
- package/src/components/ui/confirm-dialog.tsx +109 -0
- package/src/components/ui/context-menu.tsx +141 -0
- package/src/components/ui/control.ts +25 -0
- package/src/components/ui/copy-button.test.tsx +36 -0
- package/src/components/ui/copy-button.tsx +229 -0
- package/src/components/ui/dialog.tsx +152 -0
- package/src/components/ui/disclosure-caret.tsx +20 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/error-state.tsx +50 -0
- package/src/components/ui/fade-text.tsx +110 -0
- package/src/components/ui/glyph-spinner.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +37 -0
- package/src/components/ui/loader.tsx +558 -0
- package/src/components/ui/log-view.tsx +17 -0
- package/src/components/ui/pagination.tsx +114 -0
- package/src/components/ui/popover.tsx +44 -0
- package/src/components/ui/scroll-area.tsx +43 -0
- package/src/components/ui/search-field.tsx +80 -0
- package/src/components/ui/segmented-control.tsx +51 -0
- package/src/components/ui/select.tsx +92 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +116 -0
- package/src/components/ui/sidebar.tsx +674 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +49 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/components/ui/text-tab.tsx +43 -0
- package/src/components/ui/textarea.tsx +11 -0
- package/src/components/ui/tool-icon.tsx +65 -0
- package/src/components/ui/tooltip.tsx +69 -0
- package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/global.d.ts +457 -0
- package/src/hooks/use-image-download.ts +85 -0
- package/src/hooks/use-media-query.ts +24 -0
- package/src/hooks/use-mobile.ts +3 -0
- package/src/hooks/use-resize-observer.ts +38 -0
- package/src/hooks/use-worktree-info.ts +68 -0
- package/src/i18n/catalog.ts +12 -0
- package/src/i18n/context.test.tsx +232 -0
- package/src/i18n/context.tsx +183 -0
- package/src/i18n/define-locale.ts +41 -0
- package/src/i18n/en.ts +1779 -0
- package/src/i18n/index.ts +20 -0
- package/src/i18n/ja.ts +1890 -0
- package/src/i18n/languages.test.ts +43 -0
- package/src/i18n/languages.ts +86 -0
- package/src/i18n/runtime.test.ts +75 -0
- package/src/i18n/runtime.ts +53 -0
- package/src/i18n/types.ts +1452 -0
- package/src/i18n/zh-hant.ts +1849 -0
- package/src/i18n/zh.ts +1923 -0
- package/src/lib/ansi.test.ts +123 -0
- package/src/lib/ansi.ts +175 -0
- package/src/lib/chat-messages.test.ts +708 -0
- package/src/lib/chat-messages.ts +885 -0
- package/src/lib/chat-runtime.test.ts +18 -0
- package/src/lib/chat-runtime.ts +335 -0
- package/src/lib/clipboard.ts +28 -0
- package/src/lib/commit-changelog.test.ts +114 -0
- package/src/lib/commit-changelog.ts +177 -0
- package/src/lib/completion-sound.ts +519 -0
- package/src/lib/desktop-fs.test.ts +116 -0
- package/src/lib/desktop-fs.ts +113 -0
- package/src/lib/desktop-slash-commands.test.ts +126 -0
- package/src/lib/desktop-slash-commands.ts +286 -0
- package/src/lib/embedded-images.test.ts +35 -0
- package/src/lib/embedded-images.ts +60 -0
- package/src/lib/external-link.test.tsx +168 -0
- package/src/lib/external-link.tsx +303 -0
- package/src/lib/gateway-events.test.ts +27 -0
- package/src/lib/gateway-events.ts +49 -0
- package/src/lib/gateway-ws-url.test.ts +78 -0
- package/src/lib/gateway-ws-url.ts +91 -0
- package/src/lib/generated-images.test.ts +97 -0
- package/src/lib/generated-images.ts +116 -0
- package/src/lib/haptics.ts +129 -0
- package/src/lib/icons.ts +203 -0
- package/src/lib/incremental-external-store-runtime.ts +188 -0
- package/src/lib/katex-memo.ts +260 -0
- package/src/lib/keybinds/actions.ts +125 -0
- package/src/lib/keybinds/combo.test.ts +86 -0
- package/src/lib/keybinds/combo.ts +169 -0
- package/src/lib/local-preview.ts +126 -0
- package/src/lib/markdown-code.test.ts +23 -0
- package/src/lib/markdown-code.ts +195 -0
- package/src/lib/markdown-preprocess.ts +386 -0
- package/src/lib/media.remote.test.ts +58 -0
- package/src/lib/media.ts +111 -0
- package/src/lib/model-status-label.test.ts +31 -0
- package/src/lib/model-status-label.ts +103 -0
- package/src/lib/mutable-ref.ts +6 -0
- package/src/lib/preview-targets.test.ts +27 -0
- package/src/lib/preview-targets.ts +63 -0
- package/src/lib/profile-color.ts +58 -0
- package/src/lib/provider-setup-errors.test.ts +26 -0
- package/src/lib/provider-setup-errors.ts +12 -0
- package/src/lib/query-client.ts +13 -0
- package/src/lib/remend-tail.test.ts +105 -0
- package/src/lib/remend-tail.ts +108 -0
- package/src/lib/runtime-readiness.test.ts +65 -0
- package/src/lib/runtime-readiness.ts +147 -0
- package/src/lib/session-export.ts +57 -0
- package/src/lib/session-search.test.ts +58 -0
- package/src/lib/session-search.ts +19 -0
- package/src/lib/session-source.ts +62 -0
- package/src/lib/speech-text.ts +35 -0
- package/src/lib/statusbar.ts +91 -0
- package/src/lib/storage.test.ts +25 -0
- package/src/lib/storage.ts +107 -0
- package/src/lib/todos.test.ts +35 -0
- package/src/lib/todos.ts +51 -0
- package/src/lib/tool-result-summary.test.ts +106 -0
- package/src/lib/tool-result-summary.ts +467 -0
- package/src/lib/update-copy.test.ts +38 -0
- package/src/lib/update-copy.ts +44 -0
- package/src/lib/use-enter-animation.ts +100 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-playback.ts +128 -0
- package/src/lib/yolo-session.ts +26 -0
- package/src/main.tsx +43 -0
- package/src/nastech.test.ts +49 -0
- package/src/nastech.ts +718 -0
- package/src/store/activity.ts +100 -0
- package/src/store/boot.ts +91 -0
- package/src/store/clarify.test.ts +81 -0
- package/src/store/clarify.ts +69 -0
- package/src/store/command-palette.ts +20 -0
- package/src/store/compaction.test.ts +53 -0
- package/src/store/compaction.ts +38 -0
- package/src/store/completion-sound.ts +32 -0
- package/src/store/composer-input-history.test.ts +147 -0
- package/src/store/composer-input-history.ts +158 -0
- package/src/store/composer-queue.test.ts +148 -0
- package/src/store/composer-queue.ts +239 -0
- package/src/store/composer-status.test.ts +99 -0
- package/src/store/composer-status.ts +277 -0
- package/src/store/composer.test.ts +106 -0
- package/src/store/composer.ts +184 -0
- package/src/store/cron.ts +19 -0
- package/src/store/gateway.ts +290 -0
- package/src/store/haptics.ts +17 -0
- package/src/store/keybinds.ts +139 -0
- package/src/store/layout.ts +176 -0
- package/src/store/model-presets.test.ts +51 -0
- package/src/store/model-presets.ts +86 -0
- package/src/store/model-visibility.test.ts +37 -0
- package/src/store/model-visibility.ts +108 -0
- package/src/store/native-notifications.test.ts +192 -0
- package/src/store/native-notifications.ts +203 -0
- package/src/store/notifications.ts +165 -0
- package/src/store/onboarding.test.ts +372 -0
- package/src/store/onboarding.ts +866 -0
- package/src/store/panes.test.ts +146 -0
- package/src/store/panes.ts +145 -0
- package/src/store/preview.test.ts +135 -0
- package/src/store/preview.ts +466 -0
- package/src/store/profile.test.ts +89 -0
- package/src/store/profile.ts +365 -0
- package/src/store/prompts.test.ts +121 -0
- package/src/store/prompts.ts +115 -0
- package/src/store/session-switcher.test.ts +115 -0
- package/src/store/session-switcher.ts +128 -0
- package/src/store/session-sync.ts +25 -0
- package/src/store/session.test.ts +131 -0
- package/src/store/session.ts +255 -0
- package/src/store/subagents.test.ts +111 -0
- package/src/store/subagents.ts +260 -0
- package/src/store/thread-scroll.ts +46 -0
- package/src/store/todos.test.ts +47 -0
- package/src/store/todos.ts +64 -0
- package/src/store/tool-diffs.ts +23 -0
- package/src/store/tool-dismiss.ts +45 -0
- package/src/store/tool-view.ts +91 -0
- package/src/store/translucency.ts +38 -0
- package/src/store/updates.test.ts +77 -0
- package/src/store/updates.ts +315 -0
- package/src/store/voice-playback.ts +24 -0
- package/src/store/windows.test.ts +143 -0
- package/src/store/windows.ts +77 -0
- package/src/styles.css +1235 -0
- package/src/themes/color.ts +142 -0
- package/src/themes/context.tsx +339 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/install.test.ts +119 -0
- package/src/themes/install.ts +95 -0
- package/src/themes/presets.test.ts +33 -0
- package/src/themes/presets.ts +293 -0
- package/src/themes/profile-theme.test.ts +41 -0
- package/src/themes/types.ts +66 -0
- package/src/themes/use-skin-command.ts +60 -0
- package/src/themes/user-themes.test.ts +63 -0
- package/src/themes/user-themes.ts +122 -0
- package/src/themes/vscode.test.ts +171 -0
- package/src/themes/vscode.ts +343 -0
- package/src/types/nastech.ts +646 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +56 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { atom, computed } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import { translateNow } from '@/i18n'
|
|
4
|
+
import type { TodoItem, TodoStatus } from '@/lib/todos'
|
|
5
|
+
|
|
6
|
+
import { $gateway } from './gateway'
|
|
7
|
+
import { dispatchNativeNotification } from './native-notifications'
|
|
8
|
+
import { $subagentsBySession, type SubagentProgress } from './subagents'
|
|
9
|
+
import { $todosBySession } from './todos'
|
|
10
|
+
|
|
11
|
+
/** Composer status stack feed — merged todos, subagents, background per session. */
|
|
12
|
+
export type StatusItemState = 'done' | 'failed' | 'running'
|
|
13
|
+
export type StatusItemType = 'background' | 'subagent' | 'todo'
|
|
14
|
+
|
|
15
|
+
export interface ComposerStatusItem {
|
|
16
|
+
/** background: non-zero exit shown inline when failed. */
|
|
17
|
+
exitCode?: number
|
|
18
|
+
/** subagent: active tool label shown on the right. */
|
|
19
|
+
currentTool?: string
|
|
20
|
+
id: string
|
|
21
|
+
/** background process: captured stdout/stderr tail for the inline viewer. */
|
|
22
|
+
output?: string
|
|
23
|
+
/** subagent: its own stored session id — row click opens that session window
|
|
24
|
+
* (livestreamed by the gateway's child-session mirror). */
|
|
25
|
+
sessionId?: string
|
|
26
|
+
state: StatusItemState
|
|
27
|
+
title: string
|
|
28
|
+
/** todo: the full four-state status driving the row's checkmark glyph. */
|
|
29
|
+
todoStatus?: TodoStatus
|
|
30
|
+
type: StatusItemType
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Writable source for background work, synced from the gateway's process
|
|
34
|
+
// registry (`terminal(background=true)` spawns) via `process.list`.
|
|
35
|
+
export const $backgroundStatusBySession = atom<Record<string, ComposerStatusItem[]>>({})
|
|
36
|
+
|
|
37
|
+
// Rows the user X-ed away. The registry keeps finished processes around for a
|
|
38
|
+
// while, so without this every refresh would resurrect a dismissed row.
|
|
39
|
+
const dismissedBySession = new Map<string, Set<string>>()
|
|
40
|
+
|
|
41
|
+
const subToItem = (s: SubagentProgress): ComposerStatusItem => ({
|
|
42
|
+
currentTool: s.currentTool,
|
|
43
|
+
id: s.id,
|
|
44
|
+
sessionId: s.sessionId,
|
|
45
|
+
state: 'running',
|
|
46
|
+
title: s.goal,
|
|
47
|
+
type: 'subagent'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const todoToItem = (t: TodoItem): ComposerStatusItem => ({
|
|
51
|
+
id: `todo:${t.id}`,
|
|
52
|
+
state: t.status === 'in_progress' ? 'running' : 'done',
|
|
53
|
+
title: t.content,
|
|
54
|
+
todoStatus: t.status,
|
|
55
|
+
type: 'todo'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// The single thing the stack reads: a typed, merged item list per session.
|
|
59
|
+
export const $statusItemsBySession = computed(
|
|
60
|
+
[$subagentsBySession, $backgroundStatusBySession, $todosBySession],
|
|
61
|
+
(subs, background, todos) => {
|
|
62
|
+
const out: Record<string, ComposerStatusItem[]> = {}
|
|
63
|
+
|
|
64
|
+
const push = (sid: string, items: ComposerStatusItem[]) => {
|
|
65
|
+
if (items.length > 0) {
|
|
66
|
+
out[sid] = out[sid] ? [...out[sid], ...items] : items
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [sid, list] of Object.entries(todos)) {
|
|
71
|
+
push(sid, list.map(todoToItem))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [sid, list] of Object.entries(subs)) {
|
|
75
|
+
push(sid, list.filter(s => s.status === 'running' || s.status === 'queued').map(subToItem))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const [sid, list] of Object.entries(background)) {
|
|
79
|
+
push(sid, list)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Fixed render order for the groups in the stack (top → bottom, above queue).
|
|
87
|
+
const TYPE_ORDER: readonly StatusItemType[] = ['todo', 'subagent', 'background']
|
|
88
|
+
|
|
89
|
+
export interface StatusGroup {
|
|
90
|
+
items: ComposerStatusItem[]
|
|
91
|
+
type: StatusItemType
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function groupStatusItems(items: readonly ComposerStatusItem[]): StatusGroup[] {
|
|
95
|
+
const byType = new Map<StatusItemType, ComposerStatusItem[]>()
|
|
96
|
+
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
const list = byType.get(item.type)
|
|
99
|
+
|
|
100
|
+
if (list) {
|
|
101
|
+
list.push(item)
|
|
102
|
+
} else {
|
|
103
|
+
byType.set(item.type, [item])
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return TYPE_ORDER.filter(type => byType.has(type)).map(type => ({ items: byType.get(type)!, type }))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const writeBackground = (sid: string, items: ComposerStatusItem[]) => {
|
|
111
|
+
const current = $backgroundStatusBySession.get()
|
|
112
|
+
const next = { ...current }
|
|
113
|
+
|
|
114
|
+
if (items.length > 0) {
|
|
115
|
+
next[sid] = items
|
|
116
|
+
} else {
|
|
117
|
+
delete next[sid]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$backgroundStatusBySession.set(next)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// `tui_gateway` process.list entry (tools/process_registry.list_sessions + output_tail).
|
|
124
|
+
interface GatewayProcessEntry {
|
|
125
|
+
command?: string
|
|
126
|
+
exit_code?: number
|
|
127
|
+
output_tail?: string
|
|
128
|
+
session_id?: string
|
|
129
|
+
status?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const toBackgroundItem = (proc: GatewayProcessEntry): ComposerStatusItem => {
|
|
133
|
+
const exited = proc.status === 'exited'
|
|
134
|
+
const exitCode = typeof proc.exit_code === 'number' ? proc.exit_code : undefined
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
exitCode,
|
|
138
|
+
id: proc.session_id ?? '',
|
|
139
|
+
output: proc.output_tail || undefined,
|
|
140
|
+
state: exited ? (exitCode ? 'failed' : 'done') : 'running',
|
|
141
|
+
title: (proc.command ?? '').split('\n')[0]!.trim() || 'background process',
|
|
142
|
+
type: 'background'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const sameItem = (a: ComposerStatusItem, b: ComposerStatusItem) =>
|
|
147
|
+
a.state === b.state && a.title === b.title && a.output === b.output && a.exitCode === b.exitCode
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Layout-stable sync of the registry snapshot into the store: existing rows
|
|
151
|
+
* keep their position (status flips happen in place, never reorder), new
|
|
152
|
+
* processes append, dismissed ids stay gone, and unchanged rows keep their
|
|
153
|
+
* object identity so memoised rows skip re-rendering.
|
|
154
|
+
*/
|
|
155
|
+
export function reconcileBackgroundProcesses(sid: string, procs: GatewayProcessEntry[]) {
|
|
156
|
+
const dismissed = dismissedBySession.get(sid)
|
|
157
|
+
|
|
158
|
+
const fresh = new Map(
|
|
159
|
+
procs
|
|
160
|
+
.filter(proc => proc.session_id && !dismissed?.has(proc.session_id))
|
|
161
|
+
.map(proc => [proc.session_id!, toBackgroundItem(proc)])
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const prev = $backgroundStatusBySession.get()[sid] ?? []
|
|
165
|
+
|
|
166
|
+
// running → exited since the last snapshot = a background process just finished.
|
|
167
|
+
const prevState = new Map(prev.map(item => [item.id, item.state]))
|
|
168
|
+
|
|
169
|
+
for (const [id, item] of fresh) {
|
|
170
|
+
if (item.state !== 'running' && prevState.get(id) === 'running') {
|
|
171
|
+
dispatchNativeNotification({
|
|
172
|
+
body: item.title,
|
|
173
|
+
kind: 'backgroundDone',
|
|
174
|
+
sessionId: sid,
|
|
175
|
+
title: translateNow(
|
|
176
|
+
item.state === 'failed'
|
|
177
|
+
? 'notifications.native.backgroundFailedTitle'
|
|
178
|
+
: 'notifications.native.backgroundDoneTitle'
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const kept = prev.flatMap(old => {
|
|
185
|
+
const next = fresh.get(old.id)
|
|
186
|
+
fresh.delete(old.id)
|
|
187
|
+
|
|
188
|
+
return next ? [sameItem(old, next) ? old : next] : []
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const next = [...kept, ...fresh.values()]
|
|
192
|
+
|
|
193
|
+
// Dismissals only need remembering while the registry still reports the id.
|
|
194
|
+
if (dismissed) {
|
|
195
|
+
const reported = new Set(procs.map(proc => proc.session_id))
|
|
196
|
+
|
|
197
|
+
for (const id of dismissed) {
|
|
198
|
+
if (!reported.has(id)) {
|
|
199
|
+
dismissed.delete(id)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (next.length === prev.length && next.every((item, i) => item === prev[i])) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
writeBackground(sid, next)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Pull the session's live process snapshot from the gateway. */
|
|
212
|
+
export async function refreshBackgroundProcesses(sid: string): Promise<void> {
|
|
213
|
+
const gateway = $gateway.get()
|
|
214
|
+
|
|
215
|
+
if (!sid || !gateway) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = await gateway.request<{ processes?: GatewayProcessEntry[] }>('process.list', { session_id: sid })
|
|
221
|
+
|
|
222
|
+
reconcileBackgroundProcesses(sid, result?.processes ?? [])
|
|
223
|
+
} catch {
|
|
224
|
+
// Transient socket loss — the next trigger (event or poll) retries.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** X on a finished row: drop it now and keep it dropped across refreshes. */
|
|
229
|
+
export function dismissBackgroundProcess(sid: string, id: string) {
|
|
230
|
+
const dismissed = dismissedBySession.get(sid) ?? new Set<string>()
|
|
231
|
+
dismissed.add(id)
|
|
232
|
+
dismissedBySession.set(sid, dismissed)
|
|
233
|
+
|
|
234
|
+
const list = $backgroundStatusBySession.get()[sid] ?? []
|
|
235
|
+
|
|
236
|
+
writeBackground(
|
|
237
|
+
sid,
|
|
238
|
+
list.filter(item => item.id !== id)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** X on a running row: kill the process for real, then drop the row. */
|
|
243
|
+
export function stopBackgroundProcess(sid: string, id: string) {
|
|
244
|
+
void $gateway
|
|
245
|
+
.get()
|
|
246
|
+
?.request('process.kill', { process_id: id, session_id: sid })
|
|
247
|
+
.catch(() => undefined)
|
|
248
|
+
dismissBackgroundProcess(sid, id)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Rewind cleanup: a restore/edit discards the turns that spawned these
|
|
253
|
+
* processes, so they belong to an abandoned timeline. Kill the live ones and
|
|
254
|
+
* drop every row. Ids are marked dismissed so an in-flight `process.list` poll
|
|
255
|
+
* (kill is async) can't resurrect them; reconcile garbage-collects those once
|
|
256
|
+
* the registry stops reporting them.
|
|
257
|
+
*/
|
|
258
|
+
export function resetSessionBackground(sid: string) {
|
|
259
|
+
if (!sid) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const gateway = $gateway.get()
|
|
264
|
+
const list = $backgroundStatusBySession.get()[sid] ?? []
|
|
265
|
+
const dismissed = dismissedBySession.get(sid) ?? new Set<string>()
|
|
266
|
+
|
|
267
|
+
for (const item of list) {
|
|
268
|
+
dismissed.add(item.id)
|
|
269
|
+
|
|
270
|
+
if (item.state === 'running') {
|
|
271
|
+
void gateway?.request('process.kill', { process_id: item.id, session_id: sid }).catch(() => undefined)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
dismissedBySession.set(sid, dismissed)
|
|
276
|
+
writeBackground(sid, [])
|
|
277
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
$composerAttachments,
|
|
5
|
+
addComposerAttachment,
|
|
6
|
+
clearSessionDraft,
|
|
7
|
+
type ComposerAttachment,
|
|
8
|
+
removeComposerAttachment,
|
|
9
|
+
SESSION_DRAFTS_STORAGE_KEY,
|
|
10
|
+
stashSessionDraft,
|
|
11
|
+
takeSessionDraft,
|
|
12
|
+
updateComposerAttachment
|
|
13
|
+
} from './composer'
|
|
14
|
+
|
|
15
|
+
function attachment(overrides: Partial<ComposerAttachment> & Pick<ComposerAttachment, 'id'>): ComposerAttachment {
|
|
16
|
+
return { kind: 'file', label: 'doc.pdf', ...overrides }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('updateComposerAttachment', () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
$composerAttachments.set([])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('replaces an existing attachment in place', () => {
|
|
25
|
+
addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))
|
|
26
|
+
|
|
27
|
+
const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))
|
|
28
|
+
|
|
29
|
+
expect(updated).toBe(true)
|
|
30
|
+
const current = $composerAttachments.get()
|
|
31
|
+
expect(current).toHaveLength(1)
|
|
32
|
+
expect(current[0]?.attachedSessionId).toBe('sess-1')
|
|
33
|
+
expect(current[0]?.uploadState).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('does NOT resurrect an attachment the user removed mid-upload', () => {
|
|
37
|
+
// Drop → eager upload starts → user removes the chip → upload resolves.
|
|
38
|
+
// The late success must not re-add the removed attachment.
|
|
39
|
+
addComposerAttachment(attachment({ id: 'file:a', uploadState: 'uploading' }))
|
|
40
|
+
removeComposerAttachment('file:a')
|
|
41
|
+
|
|
42
|
+
const updated = updateComposerAttachment(attachment({ id: 'file:a', attachedSessionId: 'sess-1' }))
|
|
43
|
+
|
|
44
|
+
expect(updated).toBe(false)
|
|
45
|
+
expect($composerAttachments.get()).toHaveLength(0)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('session drafts', () => {
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
for (const scope of ['session-a', 'session-b', null]) {
|
|
52
|
+
clearSessionDraft(scope)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
window.localStorage.clear()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('keeps drafts isolated per session scope', () => {
|
|
59
|
+
stashSessionDraft('session-a', 'draft a', [])
|
|
60
|
+
stashSessionDraft('session-b', 'draft b', [attachment({ id: 'image:b', kind: 'image' })])
|
|
61
|
+
|
|
62
|
+
expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: 'draft a' })
|
|
63
|
+
expect(takeSessionDraft('session-b').text).toBe('draft b')
|
|
64
|
+
expect(takeSessionDraft('session-b').attachments.map(a => a.id)).toEqual(['image:b'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('scopes the unsaved new-session draft separately from real sessions', () => {
|
|
68
|
+
stashSessionDraft(null, 'new chat draft', [])
|
|
69
|
+
stashSessionDraft('session-a', 'session draft', [])
|
|
70
|
+
|
|
71
|
+
expect(takeSessionDraft(null).text).toBe('new chat draft')
|
|
72
|
+
expect(takeSessionDraft(undefined).text).toBe('new chat draft')
|
|
73
|
+
expect(takeSessionDraft('session-a').text).toBe('session draft')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('persists draft text (not attachments) to localStorage', () => {
|
|
77
|
+
stashSessionDraft('session-a', 'survives reload', [attachment({ id: 'file:a' })])
|
|
78
|
+
|
|
79
|
+
const persisted = JSON.parse(window.localStorage.getItem(SESSION_DRAFTS_STORAGE_KEY) ?? '{}') as Record<string, string>
|
|
80
|
+
|
|
81
|
+
expect(persisted['session-a']).toBe('survives reload')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('evicts empty drafts instead of leaving stale entries behind', () => {
|
|
85
|
+
stashSessionDraft('session-a', 'saved', [])
|
|
86
|
+
stashSessionDraft('session-a', ' ', [])
|
|
87
|
+
|
|
88
|
+
expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('clears a stashed draft after an accepted submit', () => {
|
|
92
|
+
stashSessionDraft('session-a', 'sent prompt', [attachment({ id: 'file:a' })])
|
|
93
|
+
clearSessionDraft('session-a')
|
|
94
|
+
|
|
95
|
+
expect(takeSessionDraft('session-a')).toEqual({ attachments: [], text: '' })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns clones so callers cannot mutate the stash', () => {
|
|
99
|
+
stashSessionDraft('session-a', 'draft', [attachment({ id: 'file:a' })])
|
|
100
|
+
|
|
101
|
+
const taken = takeSessionDraft('session-a')
|
|
102
|
+
taken.attachments[0]!.label = 'mutated'
|
|
103
|
+
|
|
104
|
+
expect(takeSessionDraft('session-a').attachments[0]?.label).toBe('doc.pdf')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { atom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
4
|
+
|
|
5
|
+
export interface ComposerAttachment {
|
|
6
|
+
id: string
|
|
7
|
+
kind: 'image' | 'file' | 'folder' | 'terminal' | 'url'
|
|
8
|
+
label: string
|
|
9
|
+
detail?: string
|
|
10
|
+
refText?: string
|
|
11
|
+
previewUrl?: string
|
|
12
|
+
path?: string
|
|
13
|
+
attachedSessionId?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const $composerDraft = atom('')
|
|
17
|
+
export const $composerAttachments = atom<ComposerAttachment[]>([])
|
|
18
|
+
export const $composerTerminalSelections = atom<Record<string, string>>({})
|
|
19
|
+
|
|
20
|
+
export function setComposerDraft(value: string) {
|
|
21
|
+
$composerDraft.set(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function appendComposerDraft(value: string) {
|
|
25
|
+
const text = value.trim()
|
|
26
|
+
|
|
27
|
+
if (!text) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const current = $composerDraft.get()
|
|
32
|
+
const separator = current && !current.endsWith('\n') ? '\n\n' : ''
|
|
33
|
+
|
|
34
|
+
$composerDraft.set(`${current}${separator}${text}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function appendComposerInline(value: string) {
|
|
38
|
+
const text = value.trim()
|
|
39
|
+
|
|
40
|
+
if (!text) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const current = $composerDraft.get().trimEnd()
|
|
45
|
+
const separator = current ? ' ' : ''
|
|
46
|
+
|
|
47
|
+
$composerDraft.set(`${current}${separator}${text}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clearComposerDraft() {
|
|
51
|
+
$composerDraft.set('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function addComposerAttachment(attachment: ComposerAttachment) {
|
|
55
|
+
const previous = $composerAttachments.get()
|
|
56
|
+
const next = upsertAttachment(previous, attachment)
|
|
57
|
+
$composerAttachments.set(next)
|
|
58
|
+
|
|
59
|
+
if (next.length > previous.length && attachment.kind !== 'url') {
|
|
60
|
+
triggerHaptic('selection')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removeComposerAttachment(id: string): ComposerAttachment | null {
|
|
65
|
+
const current = $composerAttachments.get()
|
|
66
|
+
const removed = current.find(attachment => attachment.id === id) || null
|
|
67
|
+
$composerAttachments.set(current.filter(attachment => attachment.id !== id))
|
|
68
|
+
|
|
69
|
+
return removed
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function clearComposerAttachments() {
|
|
73
|
+
$composerAttachments.set([])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const TERMINAL_REF_RE = /@terminal:(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)/g
|
|
77
|
+
|
|
78
|
+
function unquoteRefValue(raw: string) {
|
|
79
|
+
const head = raw[0]
|
|
80
|
+
const tail = raw[raw.length - 1]
|
|
81
|
+
const quoted = (head === '`' && tail === '`') || (head === '"' && tail === '"') || (head === "'" && tail === "'")
|
|
82
|
+
|
|
83
|
+
return (quoted ? raw.slice(1, -1) : raw).replace(/[,.;!?]+$/, '').trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function terminalLabelsFromDraft(draft: string) {
|
|
87
|
+
const labels: string[] = []
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
|
|
90
|
+
for (const match of draft.matchAll(TERMINAL_REF_RE)) {
|
|
91
|
+
const label = unquoteRefValue(match[1] || '')
|
|
92
|
+
|
|
93
|
+
if (!label || seen.has(label)) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
seen.add(label)
|
|
98
|
+
labels.push(label)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return labels
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function setComposerTerminalSelection(label: string, text: string) {
|
|
105
|
+
const nextLabel = label.trim()
|
|
106
|
+
const nextText = text.trim()
|
|
107
|
+
|
|
108
|
+
if (!nextLabel || !nextText) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const current = $composerTerminalSelections.get()
|
|
113
|
+
|
|
114
|
+
if (current[nextLabel] === nextText) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
$composerTerminalSelections.set({
|
|
119
|
+
...current,
|
|
120
|
+
[nextLabel]: nextText
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function reconcileComposerTerminalSelections(draft: string) {
|
|
125
|
+
const current = $composerTerminalSelections.get()
|
|
126
|
+
const labels = new Set(terminalLabelsFromDraft(draft))
|
|
127
|
+
let changed = false
|
|
128
|
+
const next: Record<string, string> = {}
|
|
129
|
+
|
|
130
|
+
for (const [label, text] of Object.entries(current)) {
|
|
131
|
+
if (!labels.has(label)) {
|
|
132
|
+
changed = true
|
|
133
|
+
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
next[label] = text
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (changed) {
|
|
141
|
+
$composerTerminalSelections.set(next)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function terminalContextBlocksFromDraft(draft: string) {
|
|
146
|
+
const labels = terminalLabelsFromDraft(draft)
|
|
147
|
+
|
|
148
|
+
if (labels.length === 0) {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const selections = $composerTerminalSelections.get()
|
|
153
|
+
|
|
154
|
+
return labels.flatMap(label => {
|
|
155
|
+
const text = selections[label]?.trim()
|
|
156
|
+
|
|
157
|
+
if (!text) {
|
|
158
|
+
return []
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `\`\`\`terminal\n${text}\n\`\`\``
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function clearComposerTerminalSelections() {
|
|
166
|
+
if (Object.keys($composerTerminalSelections.get()).length === 0) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$composerTerminalSelections.set({})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function upsertAttachment(attachments: ComposerAttachment[], attachment: ComposerAttachment) {
|
|
174
|
+
const index = attachments.findIndex(item => item.id === attachment.id)
|
|
175
|
+
|
|
176
|
+
if (index < 0) {
|
|
177
|
+
return [...attachments, attachment]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const next = [...attachments]
|
|
181
|
+
next[index] = attachment
|
|
182
|
+
|
|
183
|
+
return next
|
|
184
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { atom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import type { CronJob } from '@/types/nastech'
|
|
4
|
+
|
|
5
|
+
// Cron *jobs* (not run sessions) power the sidebar "Cron jobs" section. Listing
|
|
6
|
+
// the job — schedule, state, live next-run countdown — makes the job the
|
|
7
|
+
// first-class entity; its runs (sessions) resolve under it in the cron detail.
|
|
8
|
+
export const $cronJobs = atom<CronJob[]>([])
|
|
9
|
+
export const setCronJobs = (jobs: CronJob[]) => $cronJobs.set(jobs)
|
|
10
|
+
|
|
11
|
+
// In-place edit so the cron overlay's mutations (create/edit/delete/pause/…)
|
|
12
|
+
// land in the same atom the sidebar renders — no stale list until the next poll.
|
|
13
|
+
export const updateCronJobs = (fn: (jobs: CronJob[]) => CronJob[]) => $cronJobs.set(fn($cronJobs.get()))
|
|
14
|
+
|
|
15
|
+
// One-shot focus target: clicking "Manage" on a job sets this, then opens the
|
|
16
|
+
// cron overlay, which reads it once to select + scroll to that job. Cleared
|
|
17
|
+
// after consumption so re-opening cron normally doesn't re-focus a stale job.
|
|
18
|
+
export const $cronFocusJobId = atom<null | string>(null)
|
|
19
|
+
export const setCronFocusJobId = (id: null | string) => $cronFocusJobId.set(id)
|