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,171 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useEffect, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { SetTitlebarToolGroup } from '@/app/shell/titlebar-controls'
|
|
5
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
6
|
+
import { Tip } from '@/components/ui/tooltip'
|
|
7
|
+
import { translateNow, useI18n } from '@/i18n'
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
import {
|
|
10
|
+
$rightRailActiveTabId,
|
|
11
|
+
RIGHT_RAIL_PREVIEW_TAB_ID,
|
|
12
|
+
type RightRailTabId,
|
|
13
|
+
selectRightRailTab
|
|
14
|
+
} from '@/store/layout'
|
|
15
|
+
import {
|
|
16
|
+
$filePreviewTabs,
|
|
17
|
+
$previewReloadRequest,
|
|
18
|
+
$previewTarget,
|
|
19
|
+
closeRightRail,
|
|
20
|
+
closeRightRailTab,
|
|
21
|
+
type PreviewTarget
|
|
22
|
+
} from '@/store/preview'
|
|
23
|
+
|
|
24
|
+
import { PreviewPane } from './preview-pane'
|
|
25
|
+
|
|
26
|
+
export const PREVIEW_RAIL_MIN_WIDTH = '18rem'
|
|
27
|
+
export const PREVIEW_RAIL_MAX_WIDTH = '38rem'
|
|
28
|
+
|
|
29
|
+
const INTRINSIC = `clamp(${PREVIEW_RAIL_MIN_WIDTH}, 36vw, 32rem)`
|
|
30
|
+
|
|
31
|
+
// Track for <Pane id="preview">. Folds the intrinsic clamp with a min-floor
|
|
32
|
+
// against --chat-min-width so the chat surface never gets squeezed below it.
|
|
33
|
+
// Subtracts the project browser width so preview yields rather than crushing
|
|
34
|
+
// the chat when both right-side panes are open.
|
|
35
|
+
export const PREVIEW_RAIL_PANE_WIDTH = `min(${INTRINSIC}, max(0rem, calc(100vw - var(--pane-chat-sidebar-width) - var(--pane-file-browser-width, 0rem) - var(--chat-min-width))))`
|
|
36
|
+
|
|
37
|
+
interface ChatPreviewRailProps {
|
|
38
|
+
onRestartServer?: (url: string, context?: string) => Promise<string>
|
|
39
|
+
setTitlebarToolGroup?: SetTitlebarToolGroup
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RailTab {
|
|
43
|
+
id: RightRailTabId
|
|
44
|
+
label: string
|
|
45
|
+
target: PreviewTarget
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tabLabelFor(target: PreviewTarget): string {
|
|
49
|
+
const value = target.label || target.path || target.source || target.url
|
|
50
|
+
const tail = value.split(/[\\/]/).filter(Boolean).at(-1)
|
|
51
|
+
|
|
52
|
+
return tail || value || translateNow('preview.tab')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatPreviewRailProps) {
|
|
56
|
+
const { t } = useI18n()
|
|
57
|
+
const previewReloadRequest = useStore($previewReloadRequest)
|
|
58
|
+
const activeTabId = useStore($rightRailActiveTabId)
|
|
59
|
+
const filePreviewTabs = useStore($filePreviewTabs)
|
|
60
|
+
const previewTarget = useStore($previewTarget)
|
|
61
|
+
|
|
62
|
+
const tabs = useMemo<readonly RailTab[]>(
|
|
63
|
+
() => [
|
|
64
|
+
...(previewTarget ? [{ id: RIGHT_RAIL_PREVIEW_TAB_ID, label: t.preview.tab, target: previewTarget } as RailTab] : []),
|
|
65
|
+
...filePreviewTabs.map(({ id, target }) => ({ id, label: tabLabelFor(target), target }) as RailTab)
|
|
66
|
+
],
|
|
67
|
+
[filePreviewTabs, previewTarget, t.preview.tab]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const activeTab = tabs.find(tab => tab.id === activeTabId) ?? tabs[0]
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (activeTab && activeTab.id !== activeTabId) {
|
|
74
|
+
selectRightRailTab(activeTab.id)
|
|
75
|
+
}
|
|
76
|
+
}, [activeTab, activeTabId])
|
|
77
|
+
|
|
78
|
+
if (!activeTab) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isPreview = activeTab.id === RIGHT_RAIL_PREVIEW_TAB_ID
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-tertiary) bg-(--ui-editor-surface-background) text-(--ui-text-tertiary)">
|
|
86
|
+
<div className="group/rail-tabs flex h-(--titlebar-height) shrink-0 border-b border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background)">
|
|
87
|
+
<div
|
|
88
|
+
className="flex min-w-0 flex-1 overflow-x-auto overflow-y-hidden overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
89
|
+
role="tablist"
|
|
90
|
+
>
|
|
91
|
+
{tabs.map(tab => {
|
|
92
|
+
const active = tab.id === activeTab.id
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div
|
|
96
|
+
className={cn(
|
|
97
|
+
'group/tab relative flex h-full min-w-0 max-w-48 shrink-0 items-center text-[0.6875rem] font-medium [-webkit-app-region:no-drag] last:border-r last:border-(--ui-stroke-quaternary)',
|
|
98
|
+
active
|
|
99
|
+
? 'bg-(--ui-editor-surface-background) text-foreground [--tab-bg:var(--ui-editor-surface-background)]'
|
|
100
|
+
: 'border-r border-(--ui-stroke-quaternary) text-(--ui-text-tertiary) [--tab-bg:var(--ui-sidebar-surface-background)] hover:bg-(--chrome-action-hover) hover:text-foreground'
|
|
101
|
+
)}
|
|
102
|
+
key={tab.id}
|
|
103
|
+
// Middle-click closes the tab, matching browser/IDE muscle
|
|
104
|
+
// memory. `onMouseDown` swallows the middle-button press so
|
|
105
|
+
// Chromium doesn't switch into autoscroll mode.
|
|
106
|
+
onAuxClick={event => {
|
|
107
|
+
if (event.button !== 1) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
event.preventDefault()
|
|
112
|
+
closeRightRailTab(tab.id)
|
|
113
|
+
}}
|
|
114
|
+
onMouseDown={event => {
|
|
115
|
+
if (event.button === 1) {
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{active && (
|
|
121
|
+
<span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-(--ui-stroke-primary)" />
|
|
122
|
+
)}
|
|
123
|
+
<Tip label={tab.label}>
|
|
124
|
+
<button
|
|
125
|
+
aria-selected={active}
|
|
126
|
+
className="flex h-full min-w-0 max-w-full items-center overflow-hidden pl-3 pr-2 text-left outline-none"
|
|
127
|
+
onClick={() => selectRightRailTab(tab.id)}
|
|
128
|
+
role="tab"
|
|
129
|
+
type="button"
|
|
130
|
+
>
|
|
131
|
+
<span className="block min-w-0 truncate">{tab.label}</span>
|
|
132
|
+
</button>
|
|
133
|
+
</Tip>
|
|
134
|
+
<span
|
|
135
|
+
aria-hidden="true"
|
|
136
|
+
className="pointer-events-none absolute inset-y-0 right-0 w-9 bg-[linear-gradient(to_right,transparent,var(--tab-bg)_55%)] opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100"
|
|
137
|
+
/>
|
|
138
|
+
<button
|
|
139
|
+
aria-label={t.preview.closeTab(tab.label)}
|
|
140
|
+
className="pointer-events-none absolute right-1.5 top-1/2 grid size-4 -translate-y-1/2 place-items-center rounded-sm text-(--ui-text-tertiary) opacity-0 transition-[background-color,color,opacity] hover:bg-(--ui-bg-secondary) hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/tab:pointer-events-auto group-hover/tab:opacity-100 group-focus-within/tab:pointer-events-auto group-focus-within/tab:opacity-100"
|
|
141
|
+
onClick={() => closeRightRailTab(tab.id)}
|
|
142
|
+
type="button"
|
|
143
|
+
>
|
|
144
|
+
<Codicon name="close" size="0.75rem" />
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
<button
|
|
151
|
+
aria-label={t.preview.closePane}
|
|
152
|
+
className="mr-1.5 grid size-6 shrink-0 self-center place-items-center rounded-md text-(--ui-text-tertiary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring group-hover/rail-tabs:opacity-100 [-webkit-app-region:no-drag]"
|
|
153
|
+
onClick={closeRightRail}
|
|
154
|
+
type="button"
|
|
155
|
+
>
|
|
156
|
+
<Codicon name="close" size="0.75rem" />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
161
|
+
<PreviewPane
|
|
162
|
+
embedded
|
|
163
|
+
onRestartServer={isPreview ? onRestartServer : undefined}
|
|
164
|
+
reloadRequest={previewReloadRequest}
|
|
165
|
+
setTitlebarToolGroup={setTitlebarToolGroup}
|
|
166
|
+
target={activeTab.target}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</aside>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
|
|
5
|
+
import { $activeSessionId } from '@/store/session'
|
|
6
|
+
import { onScrollToBottomRequest, resetThreadScroll, setThreadAtBottom } from '@/store/thread-scroll'
|
|
7
|
+
|
|
8
|
+
import { ScrollToBottomButton } from './scroll-to-bottom-button'
|
|
9
|
+
|
|
10
|
+
function pendingApproval() {
|
|
11
|
+
$activeSessionId.set('sess-1')
|
|
12
|
+
setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
cleanup()
|
|
17
|
+
clearAllPrompts()
|
|
18
|
+
resetThreadScroll()
|
|
19
|
+
$activeSessionId.set(null)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// `getByRole('button')` excludes aria-hidden nodes, so "queryByRole null" is the
|
|
23
|
+
// control's hidden (parked-at-bottom) state.
|
|
24
|
+
describe('ScrollToBottomButton', () => {
|
|
25
|
+
it('stays hidden while parked at the bottom', () => {
|
|
26
|
+
render(<ScrollToBottomButton />)
|
|
27
|
+
|
|
28
|
+
expect(screen.queryByRole('button')).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('is a plain jump-to-bottom control when scrolled up with no approval', () => {
|
|
32
|
+
setThreadAtBottom(false)
|
|
33
|
+
render(<ScrollToBottomButton />)
|
|
34
|
+
|
|
35
|
+
expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeTruthy()
|
|
36
|
+
expect(screen.queryByText('Approval needed')).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('morphs into the approval pill when scrolled up with a pending approval', () => {
|
|
40
|
+
pendingApproval()
|
|
41
|
+
setThreadAtBottom(false)
|
|
42
|
+
render(<ScrollToBottomButton />)
|
|
43
|
+
|
|
44
|
+
expect(screen.getByRole('button', { name: 'Approval needed' })).toBeTruthy()
|
|
45
|
+
expect(screen.getByText('Approval needed')).toBeTruthy()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not morph while a pending approval is still in view (at bottom)', () => {
|
|
49
|
+
pendingApproval()
|
|
50
|
+
render(<ScrollToBottomButton />)
|
|
51
|
+
|
|
52
|
+
// Parked at bottom → control hidden, so it can't claim "approval needed".
|
|
53
|
+
expect(screen.queryByRole('button')).toBeNull()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('re-arms sticky-bottom on click', () => {
|
|
57
|
+
const handler = vi.fn()
|
|
58
|
+
const stop = onScrollToBottomRequest(handler)
|
|
59
|
+
setThreadAtBottom(false)
|
|
60
|
+
render(<ScrollToBottomButton />)
|
|
61
|
+
|
|
62
|
+
fireEvent.click(screen.getByRole('button'))
|
|
63
|
+
|
|
64
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
65
|
+
stop()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
5
|
+
import { useI18n } from '@/i18n'
|
|
6
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
7
|
+
import { cn } from '@/lib/utils'
|
|
8
|
+
import { $approvalRequest } from '@/store/prompts'
|
|
9
|
+
import { $threadJumpButtonVisible, requestScrollToBottom } from '@/store/thread-scroll'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Floating "jump to bottom" control. Sits centered just above the composer,
|
|
13
|
+
* clearing the out-of-flow status stack via the same measured-height CSS vars
|
|
14
|
+
* the thread's bottom clearance uses (`--composer-measured-height` +
|
|
15
|
+
* `--status-stack-measured-height`), so it never overlaps the queue / subagent
|
|
16
|
+
* / background cards. Visible only while the user has scrolled meaningfully
|
|
17
|
+
* away from the bottom; clicking re-arms sticky-bottom and pins the viewport.
|
|
18
|
+
*
|
|
19
|
+
* When the turn is BLOCKED on an approval, this same control morphs into an
|
|
20
|
+
* "Approval needed" pill — the only response surface is the inline Run/Reject
|
|
21
|
+
* bar on the parked tool row, which is always the bottom-most content, so the
|
|
22
|
+
* existing scroll-to-bottom action lands the user right on it. One control, no
|
|
23
|
+
* collision, no second scroll path (native scrollIntoView would scroll
|
|
24
|
+
* overflow:hidden ancestors that can't scroll back and wreck the layout).
|
|
25
|
+
*
|
|
26
|
+
* Enter/exit motion lives in styles.css under `.thread-jump-button` — a
|
|
27
|
+
* directional scale (contract in from 1.1, contract out to 0.9) keyed off
|
|
28
|
+
* `data-state`. `idle` (never-shown) stays silent so it can't flash on mount;
|
|
29
|
+
* `in`/`out` only swap once it has actually appeared.
|
|
30
|
+
*/
|
|
31
|
+
export function ScrollToBottomButton() {
|
|
32
|
+
const { t } = useI18n()
|
|
33
|
+
const visible = useStore($threadJumpButtonVisible)
|
|
34
|
+
const request = useStore($approvalRequest)
|
|
35
|
+
// Scrolled away while an approval is pending → the inline Run/Reject bar is
|
|
36
|
+
// below the fold. Relabel so the user knows the session needs them, not just
|
|
37
|
+
// that there's more to read.
|
|
38
|
+
const approval = visible && Boolean(request)
|
|
39
|
+
const hasShownRef = useRef(false)
|
|
40
|
+
|
|
41
|
+
if (visible) {
|
|
42
|
+
hasShownRef.current = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const state = visible ? 'in' : hasShownRef.current ? 'out' : 'idle'
|
|
46
|
+
const label = approval ? t.assistant.approval.jumpToApproval : t.assistant.thread.scrollToBottom
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
aria-hidden={!visible}
|
|
51
|
+
aria-label={label}
|
|
52
|
+
className={cn(
|
|
53
|
+
'thread-jump-button absolute left-1/2 z-20 grid place-items-center backdrop-blur-[0.75rem] [-webkit-backdrop-filter:blur(0.75rem)]',
|
|
54
|
+
approval
|
|
55
|
+
? 'h-8 grid-flow-col gap-1.5 rounded-full border border-primary/40 bg-(--composer-fill) px-3 text-primary hover:bg-primary/10'
|
|
56
|
+
: 'size-8 rounded-full border border-border/65 bg-(--composer-fill) text-muted-foreground hover:text-foreground',
|
|
57
|
+
!visible && 'pointer-events-none'
|
|
58
|
+
)}
|
|
59
|
+
data-state={state}
|
|
60
|
+
onClick={() => {
|
|
61
|
+
triggerHaptic('selection')
|
|
62
|
+
requestScrollToBottom()
|
|
63
|
+
}}
|
|
64
|
+
style={{
|
|
65
|
+
bottom: 'calc(var(--composer-measured-height) + var(--status-stack-measured-height) + 0.625rem)'
|
|
66
|
+
}}
|
|
67
|
+
tabIndex={visible ? 0 : -1}
|
|
68
|
+
type="button"
|
|
69
|
+
>
|
|
70
|
+
<Codicon name="arrow-down" size={approval ? '0.875rem' : '1rem'} />
|
|
71
|
+
{approval && <span className="text-xs font-medium">{label}</span>}
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
5
|
+
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
|
6
|
+
import { SidebarGroup, SidebarGroupContent } from '@/components/ui/sidebar'
|
|
7
|
+
import { Tip } from '@/components/ui/tooltip'
|
|
8
|
+
import { getCronJobRuns, type SessionInfo } from '@/nastech'
|
|
9
|
+
import { useI18n } from '@/i18n'
|
|
10
|
+
import { cn } from '@/lib/utils'
|
|
11
|
+
import { $selectedStoredSessionId } from '@/store/session'
|
|
12
|
+
import type { CronJob } from '@/types/nastech'
|
|
13
|
+
|
|
14
|
+
import { jobState, jobTitle, STATE_DOT } from '../../cron/job-state'
|
|
15
|
+
import { SidebarPanelLabel } from '../../shell/sidebar-label'
|
|
16
|
+
|
|
17
|
+
const INACTIVE_STATES = new Set(['completed', 'disabled', 'error', 'paused'])
|
|
18
|
+
|
|
19
|
+
// Recent runs shown in the inline quick-peek — enough to glance at history
|
|
20
|
+
// without turning the sidebar into the full Cron page.
|
|
21
|
+
const PEEK_RUN_LIMIT = 5
|
|
22
|
+
|
|
23
|
+
// Runs are written by the background scheduler tick (no UI signal), so poll the
|
|
24
|
+
// open peek so a freshly-fired run shows up within a few seconds.
|
|
25
|
+
const PEEK_POLL_INTERVAL_MS = 8000
|
|
26
|
+
|
|
27
|
+
const relativeFmt = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' })
|
|
28
|
+
|
|
29
|
+
// Localized "in 5 min" / "2 hr ago" without hand-rolled strings — picks the
|
|
30
|
+
// coarsest sensible unit so a daily job reads "in 14 hr", not "in 840 min".
|
|
31
|
+
function relativeTime(targetMs: number, nowMs: number): string {
|
|
32
|
+
const diff = targetMs - nowMs
|
|
33
|
+
const abs = Math.abs(diff)
|
|
34
|
+
const sign = diff < 0 ? -1 : 1
|
|
35
|
+
|
|
36
|
+
if (abs < 60_000) {return relativeFmt.format(sign * Math.round(abs / 1000), 'second')}
|
|
37
|
+
|
|
38
|
+
if (abs < 3_600_000) {return relativeFmt.format(sign * Math.round(abs / 60_000), 'minute')}
|
|
39
|
+
|
|
40
|
+
if (abs < 86_400_000) {return relativeFmt.format(sign * Math.round(abs / 3_600_000), 'hour')}
|
|
41
|
+
|
|
42
|
+
return relativeFmt.format(sign * Math.round(abs / 86_400_000), 'day')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nextRunMs(job: CronJob): null | number {
|
|
46
|
+
if (!job.next_run_at) {return null}
|
|
47
|
+
|
|
48
|
+
const ms = Date.parse(job.next_run_at)
|
|
49
|
+
|
|
50
|
+
return Number.isNaN(ms) ? null : ms
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Runs all belong to the same job, so the run name just repeats the job name —
|
|
54
|
+
// the timestamp is what tells them apart. Compact (no year, no seconds) for the
|
|
55
|
+
// narrow sidebar.
|
|
56
|
+
function formatRunTime(seconds?: null | number): string {
|
|
57
|
+
if (!seconds) {return '—'}
|
|
58
|
+
|
|
59
|
+
const date = new Date(seconds * 1000)
|
|
60
|
+
|
|
61
|
+
return Number.isNaN(date.valueOf())
|
|
62
|
+
? '—'
|
|
63
|
+
: date.toLocaleString(undefined, { day: 'numeric', hour: 'numeric', minute: '2-digit', month: 'short' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SidebarCronJobsSectionProps {
|
|
67
|
+
jobs: CronJob[]
|
|
68
|
+
label: string
|
|
69
|
+
max?: number
|
|
70
|
+
// Open a run session's chat (1 click to output).
|
|
71
|
+
onOpenRun: (sessionId: string) => void
|
|
72
|
+
// Open the full Cron page focused on this job (manage / full history).
|
|
73
|
+
onManageJob: (jobId: string) => void
|
|
74
|
+
// Fire the job now.
|
|
75
|
+
onTriggerJob: (jobId: string) => void
|
|
76
|
+
onToggle: () => void
|
|
77
|
+
open: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function SidebarCronJobsSection({
|
|
81
|
+
jobs,
|
|
82
|
+
label,
|
|
83
|
+
max = 50,
|
|
84
|
+
onManageJob,
|
|
85
|
+
onOpenRun,
|
|
86
|
+
onTriggerJob,
|
|
87
|
+
onToggle,
|
|
88
|
+
open
|
|
89
|
+
}: SidebarCronJobsSectionProps) {
|
|
90
|
+
const [nowMs, setNowMs] = useState(() => Date.now())
|
|
91
|
+
// Single-open inline peek so the section stays scannable.
|
|
92
|
+
const [peekJobId, setPeekJobId] = useState<null | string>(null)
|
|
93
|
+
|
|
94
|
+
// One clock for the whole section (rows are pure) so the countdowns tick
|
|
95
|
+
// without re-rendering the rest of the sidebar. Only runs while expanded.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!open) {return}
|
|
98
|
+
|
|
99
|
+
const id = window.setInterval(() => setNowMs(Date.now()), 1000)
|
|
100
|
+
|
|
101
|
+
return () => window.clearInterval(id)
|
|
102
|
+
}, [open])
|
|
103
|
+
|
|
104
|
+
// Upcoming first (soonest next run), jobs with no next run sink to the bottom,
|
|
105
|
+
// then alphabetical for stability.
|
|
106
|
+
const sorted = useMemo(() => {
|
|
107
|
+
return [...jobs].sort((a, b) => {
|
|
108
|
+
const an = nextRunMs(a)
|
|
109
|
+
const bn = nextRunMs(b)
|
|
110
|
+
|
|
111
|
+
if (an !== null && bn !== null && an !== bn) {return an - bn}
|
|
112
|
+
|
|
113
|
+
if (an === null && bn !== null) {return 1}
|
|
114
|
+
|
|
115
|
+
if (an !== null && bn === null) {return -1}
|
|
116
|
+
|
|
117
|
+
return jobTitle(a).localeCompare(jobTitle(b))
|
|
118
|
+
})
|
|
119
|
+
}, [jobs])
|
|
120
|
+
|
|
121
|
+
const shown = sorted.slice(0, max)
|
|
122
|
+
// When capped, signal "50+" rather than implying the list is complete.
|
|
123
|
+
const countLabel = jobs.length > max ? `${max}+` : String(jobs.length)
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<SidebarGroup className="shrink-0 p-0 pb-1">
|
|
127
|
+
<div className="group/section flex shrink-0 items-center justify-between pb-1 pt-1.5">
|
|
128
|
+
<button
|
|
129
|
+
className="group/section-label flex w-fit items-center gap-1 bg-transparent text-left leading-none"
|
|
130
|
+
onClick={onToggle}
|
|
131
|
+
type="button"
|
|
132
|
+
>
|
|
133
|
+
<SidebarPanelLabel>{label}</SidebarPanelLabel>
|
|
134
|
+
<span className="text-[0.6875rem] font-medium text-(--ui-text-quaternary)">{countLabel}</span>
|
|
135
|
+
<DisclosureCaret
|
|
136
|
+
className="text-(--ui-text-tertiary) opacity-0 transition group-hover/section-label:opacity-100"
|
|
137
|
+
open={open}
|
|
138
|
+
/>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
{open && (
|
|
142
|
+
<SidebarGroupContent className="flex max-h-72 shrink-0 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75">
|
|
143
|
+
{shown.map(job => (
|
|
144
|
+
<CronJobSidebarRow
|
|
145
|
+
expanded={peekJobId === job.id}
|
|
146
|
+
job={job}
|
|
147
|
+
key={job.id}
|
|
148
|
+
nowMs={nowMs}
|
|
149
|
+
onManage={() => onManageJob(job.id)}
|
|
150
|
+
onOpenRun={onOpenRun}
|
|
151
|
+
onTogglePeek={() => setPeekJobId(prev => (prev === job.id ? null : job.id))}
|
|
152
|
+
onTrigger={() => onTriggerJob(job.id)}
|
|
153
|
+
/>
|
|
154
|
+
))}
|
|
155
|
+
</SidebarGroupContent>
|
|
156
|
+
)}
|
|
157
|
+
</SidebarGroup>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function CronJobSidebarRow({
|
|
162
|
+
expanded,
|
|
163
|
+
job,
|
|
164
|
+
nowMs,
|
|
165
|
+
onManage,
|
|
166
|
+
onOpenRun,
|
|
167
|
+
onTogglePeek,
|
|
168
|
+
onTrigger
|
|
169
|
+
}: {
|
|
170
|
+
expanded: boolean
|
|
171
|
+
job: CronJob
|
|
172
|
+
nowMs: number
|
|
173
|
+
onManage: () => void
|
|
174
|
+
onOpenRun: (sessionId: string) => void
|
|
175
|
+
onTogglePeek: () => void
|
|
176
|
+
onTrigger: () => void
|
|
177
|
+
}) {
|
|
178
|
+
const { t } = useI18n()
|
|
179
|
+
const c = t.cron
|
|
180
|
+
const state = jobState(job)
|
|
181
|
+
const next = nextRunMs(job)
|
|
182
|
+
const label = jobTitle(job)
|
|
183
|
+
|
|
184
|
+
const meta = INACTIVE_STATES.has(state)
|
|
185
|
+
? (c.states[state] ?? state)
|
|
186
|
+
: next !== null
|
|
187
|
+
? relativeTime(next, nowMs)
|
|
188
|
+
: '—'
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<div className="group/cron relative grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_auto] items-center rounded-md hover:bg-(--chrome-action-hover)">
|
|
193
|
+
{/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
|
|
194
|
+
so the cron dots line up with the sessions above; the caret sits next
|
|
195
|
+
to the label (matching the other sidebar disclosures) and the whole
|
|
196
|
+
label area toggles the run peek. */}
|
|
197
|
+
<button
|
|
198
|
+
aria-expanded={expanded}
|
|
199
|
+
aria-label={expanded ? c.hideRuns : c.showRuns}
|
|
200
|
+
className="flex min-w-0 items-center gap-1.5 bg-transparent py-0.5 pl-2 pr-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
201
|
+
onClick={onTogglePeek}
|
|
202
|
+
title={label}
|
|
203
|
+
type="button"
|
|
204
|
+
>
|
|
205
|
+
<span className="grid w-3.5 shrink-0 place-items-center">
|
|
206
|
+
<span
|
|
207
|
+
aria-hidden="true"
|
|
208
|
+
className={cn(
|
|
209
|
+
'size-1 rounded-full',
|
|
210
|
+
STATE_DOT[state] ?? 'bg-(--ui-text-quaternary)',
|
|
211
|
+
state === 'running' && 'size-1.5 animate-pulse'
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
</span>
|
|
215
|
+
<span className="min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary) group-hover/cron:text-foreground">
|
|
216
|
+
{label}
|
|
217
|
+
</span>
|
|
218
|
+
<DisclosureCaret
|
|
219
|
+
className={cn(
|
|
220
|
+
'shrink-0 text-(--ui-text-tertiary) transition',
|
|
221
|
+
expanded ? 'opacity-100' : 'opacity-0 group-hover/cron:opacity-100'
|
|
222
|
+
)}
|
|
223
|
+
open={expanded}
|
|
224
|
+
/>
|
|
225
|
+
</button>
|
|
226
|
+
{/* Trailing cluster: countdown by default, quick actions on hover. */}
|
|
227
|
+
<div className="flex items-center gap-0.5 justify-self-end pr-1">
|
|
228
|
+
<span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
|
|
229
|
+
{meta}
|
|
230
|
+
</span>
|
|
231
|
+
<div className="hidden items-center gap-0.5 group-hover/cron:flex">
|
|
232
|
+
<Tip label={c.triggerNow}>
|
|
233
|
+
<button
|
|
234
|
+
aria-label={c.triggerNow}
|
|
235
|
+
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
|
236
|
+
onClick={onTrigger}
|
|
237
|
+
type="button"
|
|
238
|
+
>
|
|
239
|
+
<Codicon name="zap" size="0.75rem" />
|
|
240
|
+
</button>
|
|
241
|
+
</Tip>
|
|
242
|
+
<Tip label={c.manage}>
|
|
243
|
+
<button
|
|
244
|
+
aria-label={c.manage}
|
|
245
|
+
className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
|
246
|
+
onClick={onManage}
|
|
247
|
+
type="button"
|
|
248
|
+
>
|
|
249
|
+
<Codicon name="watch" size="0.75rem" />
|
|
250
|
+
</button>
|
|
251
|
+
</Tip>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
{expanded && <CronJobSidebarRuns jobId={job.id} onOpenRun={onOpenRun} />}
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function CronJobSidebarRuns({
|
|
261
|
+
jobId,
|
|
262
|
+
onOpenRun
|
|
263
|
+
}: {
|
|
264
|
+
jobId: string
|
|
265
|
+
onOpenRun: (sessionId: string) => void
|
|
266
|
+
}) {
|
|
267
|
+
const { t } = useI18n()
|
|
268
|
+
const c = t.cron
|
|
269
|
+
const selectedSessionId = useStore($selectedStoredSessionId)
|
|
270
|
+
const [runs, setRuns] = useState<null | SessionInfo[]>(null)
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
let cancelled = false
|
|
274
|
+
|
|
275
|
+
const load = () =>
|
|
276
|
+
getCronJobRuns(jobId, PEEK_RUN_LIMIT)
|
|
277
|
+
.then(result => {
|
|
278
|
+
if (!cancelled) {setRuns(result)}
|
|
279
|
+
})
|
|
280
|
+
.catch(() => {
|
|
281
|
+
if (!cancelled) {setRuns(prev => prev ?? [])}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
void load()
|
|
285
|
+
|
|
286
|
+
const intervalId = window.setInterval(() => {
|
|
287
|
+
if (document.visibilityState === 'visible') {void load()}
|
|
288
|
+
}, PEEK_POLL_INTERVAL_MS)
|
|
289
|
+
|
|
290
|
+
return () => {
|
|
291
|
+
cancelled = true
|
|
292
|
+
window.clearInterval(intervalId)
|
|
293
|
+
}
|
|
294
|
+
}, [jobId])
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="mb-1 ml-[1.375rem] flex flex-col gap-px">
|
|
298
|
+
{runs === null ? (
|
|
299
|
+
<div className="flex items-center gap-1.5 py-1 pl-1 text-[0.6875rem] text-(--ui-text-tertiary)">
|
|
300
|
+
<Codicon name="loading" size="0.75rem" spinning />
|
|
301
|
+
</div>
|
|
302
|
+
) : runs.length === 0 ? (
|
|
303
|
+
<div className="py-1 pl-1 text-[0.6875rem] text-(--ui-text-tertiary)">{c.noRuns}</div>
|
|
304
|
+
) : (
|
|
305
|
+
<>
|
|
306
|
+
{runs.map(run => (
|
|
307
|
+
<button
|
|
308
|
+
className={cn(
|
|
309
|
+
'truncate rounded-md px-1.5 py-0.5 text-left text-[0.6875rem] tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
310
|
+
run.id === selectedSessionId
|
|
311
|
+
? 'bg-(--ui-row-active-background) text-foreground'
|
|
312
|
+
: 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
|
|
313
|
+
)}
|
|
314
|
+
key={run.id}
|
|
315
|
+
onClick={() => onOpenRun(run.id)}
|
|
316
|
+
type="button"
|
|
317
|
+
>
|
|
318
|
+
{formatRunTime(run.last_active || run.started_at)}
|
|
319
|
+
</button>
|
|
320
|
+
))}
|
|
321
|
+
</>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
}
|