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,1219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
DndContext,
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
KeyboardSensor,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors
|
|
9
|
+
} from '@dnd-kit/core'
|
|
10
|
+
import {
|
|
11
|
+
arrayMove,
|
|
12
|
+
SortableContext,
|
|
13
|
+
sortableKeyboardCoordinates,
|
|
14
|
+
useSortable,
|
|
15
|
+
verticalListSortingStrategy
|
|
16
|
+
} from '@dnd-kit/sortable'
|
|
17
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
18
|
+
import { useStore } from '@nanostores/react'
|
|
19
|
+
import type * as React from 'react'
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
21
|
+
|
|
22
|
+
import { Button } from '@/components/ui/button'
|
|
23
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
24
|
+
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
|
|
25
|
+
import { KbdGroup } from '@/components/ui/kbd'
|
|
26
|
+
import { SearchField } from '@/components/ui/search-field'
|
|
27
|
+
import {
|
|
28
|
+
Sidebar,
|
|
29
|
+
SidebarContent,
|
|
30
|
+
SidebarGroup,
|
|
31
|
+
SidebarGroupContent,
|
|
32
|
+
SidebarMenu,
|
|
33
|
+
SidebarMenuButton,
|
|
34
|
+
SidebarMenuItem
|
|
35
|
+
} from '@/components/ui/sidebar'
|
|
36
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
37
|
+
import { Tip } from '@/components/ui/tooltip'
|
|
38
|
+
import { searchSessions, type SessionInfo, type SessionSearchResult } from '@/nastech'
|
|
39
|
+
import { useI18n } from '@/i18n'
|
|
40
|
+
import { profileColor } from '@/lib/profile-color'
|
|
41
|
+
import { sessionMatchesSearch } from '@/lib/session-search'
|
|
42
|
+
import { cn } from '@/lib/utils'
|
|
43
|
+
import { $cronJobs } from '@/store/cron'
|
|
44
|
+
import {
|
|
45
|
+
$panesFlipped,
|
|
46
|
+
$pinnedSessionIds,
|
|
47
|
+
$sidebarAgentsGrouped,
|
|
48
|
+
$sidebarCronOpen,
|
|
49
|
+
$sidebarOpen,
|
|
50
|
+
$sidebarPinsOpen,
|
|
51
|
+
$sidebarRecentsOpen,
|
|
52
|
+
pinSession,
|
|
53
|
+
reorderPinnedSession,
|
|
54
|
+
SESSION_SEARCH_FOCUS_EVENT,
|
|
55
|
+
setSidebarAgentsGrouped,
|
|
56
|
+
setSidebarCronOpen,
|
|
57
|
+
setSidebarPinsOpen,
|
|
58
|
+
setSidebarRecentsOpen,
|
|
59
|
+
SIDEBAR_SESSIONS_PAGE_SIZE,
|
|
60
|
+
unpinSession
|
|
61
|
+
} from '@/store/layout'
|
|
62
|
+
import {
|
|
63
|
+
$newChatProfile,
|
|
64
|
+
$profiles,
|
|
65
|
+
$profileScope,
|
|
66
|
+
ALL_PROFILES,
|
|
67
|
+
newSessionInProfile,
|
|
68
|
+
normalizeProfileKey
|
|
69
|
+
} from '@/store/profile'
|
|
70
|
+
import {
|
|
71
|
+
$cronSessions,
|
|
72
|
+
$selectedStoredSessionId,
|
|
73
|
+
$sessionProfileTotals,
|
|
74
|
+
$sessions,
|
|
75
|
+
$sessionsLoading,
|
|
76
|
+
$sessionsTotal,
|
|
77
|
+
$workingSessionIds,
|
|
78
|
+
sessionPinId
|
|
79
|
+
} from '@/store/session'
|
|
80
|
+
|
|
81
|
+
import { type AppView, ARTIFACTS_ROUTE, MESSAGING_ROUTE, SKILLS_ROUTE } from '../../routes'
|
|
82
|
+
import { SidebarPanelLabel } from '../../shell/sidebar-label'
|
|
83
|
+
import type { SidebarNavItem } from '../../types'
|
|
84
|
+
|
|
85
|
+
import { SidebarCronJobsSection } from './cron-jobs-section'
|
|
86
|
+
import { ProfileRail } from './profile-switcher'
|
|
87
|
+
import { SidebarSessionRow } from './session-row'
|
|
88
|
+
import { VirtualSessionList } from './virtual-session-list'
|
|
89
|
+
|
|
90
|
+
const VIRTUALIZE_THRESHOLD = 25
|
|
91
|
+
|
|
92
|
+
// Render the modifier key the user actually presses on this platform. The
|
|
93
|
+
// global accelerator is bound to both Cmd+N (macOS) and Ctrl+N (everywhere
|
|
94
|
+
// else) in desktop-controller.tsx, but the hint should match muscle memory.
|
|
95
|
+
const NEW_SESSION_KBD: readonly string[] =
|
|
96
|
+
typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac') ? ['⌘', 'N'] : ['Ctrl', 'N']
|
|
97
|
+
|
|
98
|
+
const SIDEBAR_NAV: SidebarNavItem[] = [
|
|
99
|
+
{
|
|
100
|
+
id: 'new-session',
|
|
101
|
+
label: '',
|
|
102
|
+
icon: props => <Codicon name="robot" {...props} />,
|
|
103
|
+
action: 'new-session'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'skills',
|
|
107
|
+
label: '',
|
|
108
|
+
icon: props => <Codicon name="symbol-misc" {...props} />,
|
|
109
|
+
route: SKILLS_ROUTE
|
|
110
|
+
},
|
|
111
|
+
{ id: 'messaging', label: '', icon: props => <Codicon name="comment" {...props} />, route: MESSAGING_ROUTE },
|
|
112
|
+
{ id: 'artifacts', label: '', icon: props => <Codicon name="files" {...props} />, route: ARTIFACTS_ROUTE }
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const WORKSPACE_PAGE = 5
|
|
116
|
+
// ALL-profiles view: show only the latest N per profile up front to keep the
|
|
117
|
+
// unified list scannable, then reveal/fetch more in N-sized steps on demand.
|
|
118
|
+
const PROFILE_INITIAL_PAGE = 5
|
|
119
|
+
const WS_ID_PREFIX = 'workspace:'
|
|
120
|
+
|
|
121
|
+
const wsId = (id: string) => `${WS_ID_PREFIX}${id}`
|
|
122
|
+
const parseWsId = (id: string) => (id.startsWith(WS_ID_PREFIX) ? id.slice(WS_ID_PREFIX.length) : null)
|
|
123
|
+
const countLabel = (loaded: number, total: number) => (total > loaded ? `${loaded}/${total}` : String(loaded))
|
|
124
|
+
const sessionTime = (s: SessionInfo) => s.last_active || s.started_at || 0
|
|
125
|
+
|
|
126
|
+
function orderByIds<T>(items: T[], getId: (item: T) => string, orderIds: string[]): T[] {
|
|
127
|
+
if (!orderIds.length) {
|
|
128
|
+
return items
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const byId = new Map(items.map(item => [getId(item), item]))
|
|
132
|
+
const seen = new Set<string>()
|
|
133
|
+
const out: T[] = []
|
|
134
|
+
|
|
135
|
+
for (const id of orderIds) {
|
|
136
|
+
const item = byId.get(id)
|
|
137
|
+
|
|
138
|
+
if (item) {
|
|
139
|
+
out.push(item)
|
|
140
|
+
seen.add(id)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const item of items) {
|
|
145
|
+
if (!seen.has(getId(item))) {
|
|
146
|
+
out.push(item)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const baseName = (path: string) =>
|
|
154
|
+
path
|
|
155
|
+
.replace(/[/\\]+$/, '')
|
|
156
|
+
.split(/[/\\]/)
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.pop()
|
|
159
|
+
|
|
160
|
+
// FTS results cover sessions that aren't in the loaded page; synthesize a
|
|
161
|
+
// minimal SessionInfo so they render in the same row component (resume works
|
|
162
|
+
// by id; the snippet stands in for the preview).
|
|
163
|
+
function searchResultToSession(result: SessionSearchResult): SessionInfo {
|
|
164
|
+
const ts = result.session_started ?? Date.now() / 1000
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
archived: false,
|
|
168
|
+
cwd: null,
|
|
169
|
+
ended_at: null,
|
|
170
|
+
id: result.session_id,
|
|
171
|
+
_lineage_root_id: result.lineage_root ?? null,
|
|
172
|
+
input_tokens: 0,
|
|
173
|
+
is_active: false,
|
|
174
|
+
last_active: ts,
|
|
175
|
+
message_count: 0,
|
|
176
|
+
model: result.model ?? null,
|
|
177
|
+
output_tokens: 0,
|
|
178
|
+
preview: result.snippet?.trim() || null,
|
|
179
|
+
source: result.source ?? null,
|
|
180
|
+
started_at: ts,
|
|
181
|
+
title: null,
|
|
182
|
+
tool_call_count: 0
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function workspaceGroupsFor(sessions: SessionInfo[], noWorkspaceLabel: string): SidebarSessionGroup[] {
|
|
187
|
+
const groups = new Map<string, SidebarSessionGroup>()
|
|
188
|
+
|
|
189
|
+
for (const session of sessions) {
|
|
190
|
+
const path = session.cwd?.trim() || ''
|
|
191
|
+
const id = path || '__no_workspace__'
|
|
192
|
+
const label = baseName(path) || path || noWorkspaceLabel
|
|
193
|
+
|
|
194
|
+
const group = groups.get(id) ?? { id, label, path: path || null, sessions: [] }
|
|
195
|
+
group.sessions.push(session)
|
|
196
|
+
groups.set(id, group)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Groups keep recency order (Map insertion = first-seen in the recency-sorted
|
|
200
|
+
// input, so an active project floats up), but rows *within* a group sort by
|
|
201
|
+
// creation time so they don't reshuffle every time a message lands — keeps
|
|
202
|
+
// muscle memory intact.
|
|
203
|
+
for (const group of groups.values()) {
|
|
204
|
+
group.sessions.sort((a, b) => b.started_at - a.started_at)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [...groups.values()]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function useSortableBindings(id: string) {
|
|
211
|
+
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({ id })
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
dragging: isDragging,
|
|
215
|
+
dragHandleProps: { ...attributes, ...listeners },
|
|
216
|
+
ref: setNodeRef,
|
|
217
|
+
reorderable: true as const,
|
|
218
|
+
style: { transform: CSS.Transform.toString(transform), transition }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface ChatSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|
223
|
+
currentView: AppView
|
|
224
|
+
onNavigate: (item: SidebarNavItem) => void
|
|
225
|
+
onLoadMoreSessions: () => void
|
|
226
|
+
onLoadMoreProfileSessions?: (profile: string) => Promise<void> | void
|
|
227
|
+
onResumeSession: (sessionId: string) => void
|
|
228
|
+
onDeleteSession: (sessionId: string) => void
|
|
229
|
+
onArchiveSession: (sessionId: string) => void
|
|
230
|
+
onNewSessionInWorkspace: (path: null | string) => void
|
|
231
|
+
onManageCronJob: (jobId: string) => void
|
|
232
|
+
onTriggerCronJob: (jobId: string) => void
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function ChatSidebar({
|
|
236
|
+
currentView,
|
|
237
|
+
onNavigate,
|
|
238
|
+
onLoadMoreSessions,
|
|
239
|
+
onLoadMoreProfileSessions,
|
|
240
|
+
onResumeSession,
|
|
241
|
+
onDeleteSession,
|
|
242
|
+
onArchiveSession,
|
|
243
|
+
onNewSessionInWorkspace,
|
|
244
|
+
onManageCronJob,
|
|
245
|
+
onTriggerCronJob
|
|
246
|
+
}: ChatSidebarProps) {
|
|
247
|
+
const { t } = useI18n()
|
|
248
|
+
const s = t.sidebar
|
|
249
|
+
const sidebarOpen = useStore($sidebarOpen)
|
|
250
|
+
const panesFlipped = useStore($panesFlipped)
|
|
251
|
+
const agentsGrouped = useStore($sidebarAgentsGrouped)
|
|
252
|
+
const pinnedSessionIds = useStore($pinnedSessionIds)
|
|
253
|
+
const pinsOpen = useStore($sidebarPinsOpen)
|
|
254
|
+
const agentsOpen = useStore($sidebarRecentsOpen)
|
|
255
|
+
const cronOpen = useStore($sidebarCronOpen)
|
|
256
|
+
const selectedSessionId = useStore($selectedStoredSessionId)
|
|
257
|
+
const sessions = useStore($sessions)
|
|
258
|
+
const cronSessions = useStore($cronSessions)
|
|
259
|
+
const cronJobs = useStore($cronJobs)
|
|
260
|
+
const sessionsLoading = useStore($sessionsLoading)
|
|
261
|
+
const sessionsTotal = useStore($sessionsTotal)
|
|
262
|
+
const sessionProfileTotals = useStore($sessionProfileTotals)
|
|
263
|
+
const workingSessionIds = useStore($workingSessionIds)
|
|
264
|
+
const profiles = useStore($profiles)
|
|
265
|
+
const profileScope = useStore($profileScope)
|
|
266
|
+
// Only surface the profile switcher when more than one profile exists, so
|
|
267
|
+
// single-profile users see the unchanged sidebar.
|
|
268
|
+
const multiProfile = profiles.length > 1
|
|
269
|
+
// Gate ALL-profiles grouping on multiProfile too: if a user drops back to one
|
|
270
|
+
// profile while scope is still ALL (persisted), the rail is hidden and they'd
|
|
271
|
+
// otherwise be stuck in the grouped view with no way out.
|
|
272
|
+
const showAllProfiles = multiProfile && profileScope === ALL_PROFILES
|
|
273
|
+
const [agentOrderIds, setAgentOrderIds] = useState<string[]>([])
|
|
274
|
+
const [workspaceOrderIds, setWorkspaceOrderIds] = useState<string[]>([])
|
|
275
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
276
|
+
const [serverMatches, setServerMatches] = useState<SessionSearchResult[]>([])
|
|
277
|
+
const [newSessionKbdFlash, setNewSessionKbdFlash] = useState(false)
|
|
278
|
+
const [profileLoadMorePending, setProfileLoadMorePending] = useState<Record<string, boolean>>({})
|
|
279
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
280
|
+
const trimmedQuery = searchQuery.trim()
|
|
281
|
+
|
|
282
|
+
// Hotkey (session.focusSearch) → focus the field once it's mounted.
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const onFocus = () => searchInputRef.current?.focus({ preventScroll: true })
|
|
285
|
+
|
|
286
|
+
window.addEventListener(SESSION_SEARCH_FOCUS_EVENT, onFocus)
|
|
287
|
+
|
|
288
|
+
return () => window.removeEventListener(SESSION_SEARCH_FOCUS_EVENT, onFocus)
|
|
289
|
+
}, [])
|
|
290
|
+
|
|
291
|
+
// Flash the ⌘N hint full-opacity (no transition) for the press, so hitting
|
|
292
|
+
// the shortcut visibly pings its affordance in the sidebar.
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
295
|
+
|
|
296
|
+
const onShortcut = () => {
|
|
297
|
+
setNewSessionKbdFlash(true)
|
|
298
|
+
clearTimeout(timeout)
|
|
299
|
+
timeout = setTimeout(() => setNewSessionKbdFlash(false), 140)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
window.addEventListener('NASTECH:new-session-shortcut', onShortcut)
|
|
303
|
+
|
|
304
|
+
return () => {
|
|
305
|
+
window.removeEventListener('NASTECH:new-session-shortcut', onShortcut)
|
|
306
|
+
clearTimeout(timeout)
|
|
307
|
+
}
|
|
308
|
+
}, [])
|
|
309
|
+
|
|
310
|
+
const activeSidebarSessionId = currentView === 'chat' ? selectedSessionId : null
|
|
311
|
+
|
|
312
|
+
const dndSensors = useSensors(
|
|
313
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
314
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// Profile scope = the "workspace switcher" context. Concrete scope shows only
|
|
318
|
+
// that profile's sessions (clean rows, no per-row tags); ALL fans every
|
|
319
|
+
// profile in, grouped by profile below. Single-profile users land here with
|
|
320
|
+
// scope === their only profile, so nothing is filtered out.
|
|
321
|
+
const visibleSessions = useMemo(
|
|
322
|
+
() => (showAllProfiles ? sessions : sessions.filter(s => normalizeProfileKey(s.profile) === profileScope)),
|
|
323
|
+
[sessions, showAllProfiles, profileScope]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const sortedSessions = useMemo(
|
|
327
|
+
() => [...visibleSessions].sort((a, b) => sessionTime(b) - sessionTime(a)),
|
|
328
|
+
[visibleSessions]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
const workingSessionIdSet = useMemo(() => new Set(workingSessionIds), [workingSessionIds])
|
|
332
|
+
|
|
333
|
+
// Index sessions by both their live id and their lineage-root id so a pin
|
|
334
|
+
// stored as the pre-compression root resolves to the live continuation tip.
|
|
335
|
+
const sessionByAnyId = useMemo(() => {
|
|
336
|
+
const map = new Map<string, SessionInfo>()
|
|
337
|
+
|
|
338
|
+
// Cron sessions are listed separately but can still be pinned, so index
|
|
339
|
+
// them too — otherwise a pinned cron job can't resolve into the Pinned
|
|
340
|
+
// section. Recents take precedence on id collisions (set last).
|
|
341
|
+
for (const s of [...cronSessions, ...visibleSessions]) {
|
|
342
|
+
map.set(s.id, s)
|
|
343
|
+
|
|
344
|
+
if (s._lineage_root_id && !map.has(s._lineage_root_id)) {
|
|
345
|
+
map.set(s._lineage_root_id, s)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return map
|
|
350
|
+
}, [visibleSessions, cronSessions])
|
|
351
|
+
|
|
352
|
+
const pinnedSessions = useMemo(() => {
|
|
353
|
+
const seen = new Set<string>()
|
|
354
|
+
const out: SessionInfo[] = []
|
|
355
|
+
|
|
356
|
+
for (const pinId of pinnedSessionIds) {
|
|
357
|
+
const session = sessionByAnyId.get(pinId)
|
|
358
|
+
|
|
359
|
+
if (session && !seen.has(session.id)) {
|
|
360
|
+
seen.add(session.id)
|
|
361
|
+
out.push(session)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return out
|
|
366
|
+
}, [pinnedSessionIds, sessionByAnyId])
|
|
367
|
+
|
|
368
|
+
const pinnedRealIdSet = useMemo(() => new Set(pinnedSessions.map(s => s.id)), [pinnedSessions])
|
|
369
|
+
|
|
370
|
+
// Full-text search across *all* sessions (not just the loaded page) so 699
|
|
371
|
+
// sessions stay findable. Debounced; loaded sessions are matched instantly
|
|
372
|
+
// client-side and merged ahead of the server hits.
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (!trimmedQuery) {
|
|
375
|
+
setServerMatches([])
|
|
376
|
+
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let cancelled = false
|
|
381
|
+
|
|
382
|
+
const id = window.setTimeout(() => {
|
|
383
|
+
void searchSessions(trimmedQuery)
|
|
384
|
+
.then(res => {
|
|
385
|
+
if (!cancelled) {
|
|
386
|
+
setServerMatches(res.results)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
.catch(() => undefined)
|
|
390
|
+
}, 200)
|
|
391
|
+
|
|
392
|
+
return () => {
|
|
393
|
+
cancelled = true
|
|
394
|
+
window.clearTimeout(id)
|
|
395
|
+
}
|
|
396
|
+
}, [trimmedQuery])
|
|
397
|
+
|
|
398
|
+
const searchResults = useMemo(() => {
|
|
399
|
+
if (!trimmedQuery) {
|
|
400
|
+
return []
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const out = new Map<string, SessionInfo>()
|
|
404
|
+
|
|
405
|
+
for (const s of sortedSessions) {
|
|
406
|
+
if (sessionMatchesSearch(s, trimmedQuery)) {
|
|
407
|
+
out.set(s.id, s)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const match of serverMatches) {
|
|
412
|
+
if (out.has(match.session_id)) {
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const loaded = sessionByAnyId.get(match.session_id)
|
|
417
|
+
out.set(match.session_id, loaded ?? searchResultToSession(match))
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return [...out.values()]
|
|
421
|
+
}, [trimmedQuery, sortedSessions, serverMatches, sessionByAnyId])
|
|
422
|
+
|
|
423
|
+
const unpinnedAgentSessions = useMemo(
|
|
424
|
+
() => sortedSessions.filter(s => !pinnedRealIdSet.has(s.id)),
|
|
425
|
+
[sortedSessions, pinnedRealIdSet]
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
const agentSessions = useMemo(
|
|
429
|
+
() => orderByIds(unpinnedAgentSessions, s => s.id, agentOrderIds),
|
|
430
|
+
[unpinnedAgentSessions, agentOrderIds]
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
const agentGroups = useMemo(
|
|
434
|
+
() => orderByIds(workspaceGroupsFor(agentSessions, s.noWorkspace), g => g.id, workspaceOrderIds),
|
|
435
|
+
[agentSessions, s.noWorkspace, workspaceOrderIds]
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const loadMoreForProfileGroup = useCallback(
|
|
439
|
+
(profile: string) => {
|
|
440
|
+
if (!onLoadMoreProfileSessions) {
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
setProfileLoadMorePending(prev => ({ ...prev, [profile]: true }))
|
|
445
|
+
|
|
446
|
+
void Promise.resolve(onLoadMoreProfileSessions(profile))
|
|
447
|
+
.catch(() => undefined)
|
|
448
|
+
.finally(() =>
|
|
449
|
+
setProfileLoadMorePending(({ [profile]: _done, ...rest }) => rest)
|
|
450
|
+
)
|
|
451
|
+
},
|
|
452
|
+
[onLoadMoreProfileSessions]
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
// ALL-profiles view: one collapsible group per profile, color on the header
|
|
456
|
+
// (not on every row). Default profile floats to the top, the rest alpha.
|
|
457
|
+
const profileGroups = useMemo<SidebarSessionGroup[] | undefined>(() => {
|
|
458
|
+
if (!showAllProfiles) {
|
|
459
|
+
return undefined
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const groups = new Map<string, SidebarSessionGroup>()
|
|
463
|
+
|
|
464
|
+
for (const session of agentSessions) {
|
|
465
|
+
const key = normalizeProfileKey(session.profile)
|
|
466
|
+
|
|
467
|
+
const group = groups.get(key) ?? {
|
|
468
|
+
color: profileColor(key),
|
|
469
|
+
id: key,
|
|
470
|
+
label: key,
|
|
471
|
+
mode: 'profile',
|
|
472
|
+
path: null,
|
|
473
|
+
sessions: []
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
group.sessions.push(session)
|
|
477
|
+
|
|
478
|
+
groups.set(key, group)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return [...groups.values()]
|
|
482
|
+
.map(group => ({
|
|
483
|
+
...group,
|
|
484
|
+
loadingMore: Boolean(profileLoadMorePending[group.id]),
|
|
485
|
+
onLoadMore: onLoadMoreProfileSessions ? () => loadMoreForProfileGroup(group.id) : undefined,
|
|
486
|
+
totalCount: Math.max(group.sessions.length, sessionProfileTotals[group.id] ?? 0)
|
|
487
|
+
}))
|
|
488
|
+
// default (root) first, then the rest alphabetically.
|
|
489
|
+
.sort((a, b) => (a.id === 'default' ? -1 : b.id === 'default' ? 1 : a.label.localeCompare(b.label)))
|
|
490
|
+
}, [
|
|
491
|
+
showAllProfiles,
|
|
492
|
+
agentSessions,
|
|
493
|
+
loadMoreForProfileGroup,
|
|
494
|
+
onLoadMoreProfileSessions,
|
|
495
|
+
profileLoadMorePending,
|
|
496
|
+
sessionProfileTotals
|
|
497
|
+
])
|
|
498
|
+
|
|
499
|
+
const showSessionSkeletons = sessionsLoading && sortedSessions.length === 0
|
|
500
|
+
|
|
501
|
+
const showSessionSections = showSessionSkeletons || sortedSessions.length > 0
|
|
502
|
+
|
|
503
|
+
// Pagination is scope-aware. In "All profiles" mode it tracks the global
|
|
504
|
+
// unified set. When scoped to one profile it must compare that profile's own
|
|
505
|
+
// loaded rows against that profile's total — otherwise a huge default profile
|
|
506
|
+
// keeps "Load more" stuck on while you browse a small one (the aggregator's
|
|
507
|
+
// total sums every profile). Per-profile totals come from the aggregator
|
|
508
|
+
// (children excluded); fall back to the global total / loaded count.
|
|
509
|
+
const loadedSessionCount = showAllProfiles ? sessions.length : visibleSessions.length
|
|
510
|
+
const scopedProfileTotal = showAllProfiles ? undefined : sessionProfileTotals[profileScope]
|
|
511
|
+
|
|
512
|
+
const knownSessionTotal = Math.max(
|
|
513
|
+
showAllProfiles ? sessionsTotal : (scopedProfileTotal ?? loadedSessionCount),
|
|
514
|
+
loadedSessionCount
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
const hasMoreSessions = knownSessionTotal > loadedSessionCount
|
|
518
|
+
const remainingSessionCount = Math.max(0, knownSessionTotal - loadedSessionCount)
|
|
519
|
+
|
|
520
|
+
const recentsMeta = countLabel(agentSessions.length, knownSessionTotal)
|
|
521
|
+
|
|
522
|
+
const handlePinnedDragEnd = ({ active, over }: DragEndEvent) => {
|
|
523
|
+
if (!over || active.id === over.id) {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const newIndex = pinnedSessions.findIndex(s => s.id === String(over.id))
|
|
528
|
+
|
|
529
|
+
if (newIndex < 0) {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Sortable ids are live session ids; the pinned store is keyed by durable
|
|
534
|
+
// (lineage-root) ids, so translate before reordering.
|
|
535
|
+
const dragged = sessionByAnyId.get(String(active.id))
|
|
536
|
+
reorderPinnedSession(dragged ? sessionPinId(dragged) : String(active.id), newIndex)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const handleAgentDragEnd = ({ active, over }: DragEndEvent) => {
|
|
540
|
+
if (!over || active.id === over.id) {
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const activeId = String(active.id)
|
|
545
|
+
const overId = String(over.id)
|
|
546
|
+
const activeWs = parseWsId(activeId)
|
|
547
|
+
const overWs = parseWsId(overId)
|
|
548
|
+
|
|
549
|
+
if (activeWs && overWs) {
|
|
550
|
+
const oldIdx = agentGroups.findIndex(g => g.id === activeWs)
|
|
551
|
+
const newIdx = agentGroups.findIndex(g => g.id === overWs)
|
|
552
|
+
|
|
553
|
+
if (oldIdx < 0 || newIdx < 0) {
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
setWorkspaceOrderIds(arrayMove(agentGroups, oldIdx, newIdx).map(g => g.id))
|
|
558
|
+
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (activeWs || overWs) {
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const oldIdx = agentSessions.findIndex(s => s.id === activeId)
|
|
567
|
+
const newIdx = agentSessions.findIndex(s => s.id === overId)
|
|
568
|
+
|
|
569
|
+
if (oldIdx < 0 || newIdx < 0) {
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
setAgentOrderIds(arrayMove(agentSessions, oldIdx, newIdx).map(s => s.id))
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return (
|
|
577
|
+
<Sidebar
|
|
578
|
+
className={cn(
|
|
579
|
+
'relative h-full min-w-0 overflow-hidden border-t-0 border-b-0 text-foreground transition-none',
|
|
580
|
+
panesFlipped ? 'border-l border-r-0' : 'border-r border-l-0',
|
|
581
|
+
sidebarOpen
|
|
582
|
+
? 'border-(--sidebar-edge-border) bg-(--ui-sidebar-surface-background) opacity-100'
|
|
583
|
+
: 'pointer-events-none border-transparent bg-transparent opacity-0'
|
|
584
|
+
)}
|
|
585
|
+
collapsible="none"
|
|
586
|
+
>
|
|
587
|
+
<SidebarContent className="gap-0 overflow-hidden bg-transparent px-2.5">
|
|
588
|
+
<SidebarGroup className="shrink-0 p-0 pb-2 pt-[calc(var(--titlebar-height)+0.375rem)]">
|
|
589
|
+
<SidebarGroupContent>
|
|
590
|
+
<SidebarMenu className="gap-px">
|
|
591
|
+
{SIDEBAR_NAV.map(item => {
|
|
592
|
+
const isInteractive = Boolean(item.action) || Boolean(item.route)
|
|
593
|
+
|
|
594
|
+
const active =
|
|
595
|
+
(item.id === 'skills' && currentView === 'skills') ||
|
|
596
|
+
(item.id === 'messaging' && currentView === 'messaging') ||
|
|
597
|
+
(item.id === 'artifacts' && currentView === 'artifacts')
|
|
598
|
+
|
|
599
|
+
const isNewSession = item.id === 'new-session'
|
|
600
|
+
|
|
601
|
+
return (
|
|
602
|
+
<SidebarMenuItem key={item.id}>
|
|
603
|
+
<SidebarMenuButton
|
|
604
|
+
aria-disabled={!isInteractive}
|
|
605
|
+
className={cn(
|
|
606
|
+
'flex h-7 w-full justify-start gap-2 rounded-md border border-transparent px-2 text-left text-[0.8125rem] font-medium text-(--ui-text-secondary) transition-colors duration-100 ease-out hover:bg-(--ui-control-hover-background) hover:text-foreground hover:transition-none',
|
|
607
|
+
active &&
|
|
608
|
+
'border-(--ui-stroke-tertiary) bg-(--ui-control-active-background) text-foreground shadow-none hover:border-(--ui-stroke-tertiary)!',
|
|
609
|
+
!isInteractive &&
|
|
610
|
+
'cursor-default hover:border-transparent hover:bg-transparent hover:text-inherit'
|
|
611
|
+
)}
|
|
612
|
+
onClick={() => {
|
|
613
|
+
// A plain new session lands in whatever profile the live
|
|
614
|
+
// gateway is on (= the active switcher context). null →
|
|
615
|
+
// no swap. The switcher header is the single place to
|
|
616
|
+
// change which profile that is.
|
|
617
|
+
if (isNewSession) {
|
|
618
|
+
$newChatProfile.set(null)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
onNavigate(item)
|
|
622
|
+
}}
|
|
623
|
+
tooltip={s.nav[item.id] ?? item.label}
|
|
624
|
+
type="button"
|
|
625
|
+
>
|
|
626
|
+
<item.icon className="size-4 shrink-0 text-[color-mix(in_srgb,currentColor_72%,transparent)]" />
|
|
627
|
+
{sidebarOpen && (
|
|
628
|
+
<>
|
|
629
|
+
<span className="min-w-0 flex-1 truncate max-[46.25rem]:hidden">
|
|
630
|
+
{s.nav[item.id] ?? item.label}
|
|
631
|
+
</span>
|
|
632
|
+
{isNewSession && (
|
|
633
|
+
<KbdGroup
|
|
634
|
+
className={cn('ml-auto max-[46.25rem]:hidden', newSessionKbdFlash && 'opacity-100!')}
|
|
635
|
+
keys={[...NEW_SESSION_KBD]}
|
|
636
|
+
/>
|
|
637
|
+
)}
|
|
638
|
+
</>
|
|
639
|
+
)}
|
|
640
|
+
</SidebarMenuButton>
|
|
641
|
+
</SidebarMenuItem>
|
|
642
|
+
)
|
|
643
|
+
})}
|
|
644
|
+
</SidebarMenu>
|
|
645
|
+
</SidebarGroupContent>
|
|
646
|
+
</SidebarGroup>
|
|
647
|
+
|
|
648
|
+
{sidebarOpen && showSessionSections && (
|
|
649
|
+
<div className="shrink-0 px-2 pb-1 pt-1">
|
|
650
|
+
<SearchField
|
|
651
|
+
aria-label={s.searchAria}
|
|
652
|
+
inputRef={searchInputRef}
|
|
653
|
+
onChange={setSearchQuery}
|
|
654
|
+
placeholder={s.searchPlaceholder}
|
|
655
|
+
value={searchQuery}
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
658
|
+
)}
|
|
659
|
+
|
|
660
|
+
{sidebarOpen && showSessionSections && trimmedQuery && (
|
|
661
|
+
<SidebarSessionsSection
|
|
662
|
+
activeSessionId={activeSidebarSessionId}
|
|
663
|
+
contentClassName="flex min-h-0 flex-1 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75"
|
|
664
|
+
emptyState={
|
|
665
|
+
<div className="grid min-h-24 place-items-center rounded-lg px-2 text-center text-xs text-(--ui-text-tertiary)">
|
|
666
|
+
{s.noMatch(trimmedQuery)}
|
|
667
|
+
</div>
|
|
668
|
+
}
|
|
669
|
+
label={s.results}
|
|
670
|
+
labelMeta={String(searchResults.length)}
|
|
671
|
+
onArchiveSession={onArchiveSession}
|
|
672
|
+
onDeleteSession={onDeleteSession}
|
|
673
|
+
onResumeSession={onResumeSession}
|
|
674
|
+
onToggle={() => undefined}
|
|
675
|
+
onTogglePin={pinSession}
|
|
676
|
+
open
|
|
677
|
+
pinned={false}
|
|
678
|
+
rootClassName="min-h-0 flex-1 p-0"
|
|
679
|
+
sessions={searchResults}
|
|
680
|
+
workingSessionIdSet={workingSessionIdSet}
|
|
681
|
+
/>
|
|
682
|
+
)}
|
|
683
|
+
|
|
684
|
+
{sidebarOpen && showSessionSections && !trimmedQuery && (
|
|
685
|
+
<SidebarSessionsSection
|
|
686
|
+
activeSessionId={activeSidebarSessionId}
|
|
687
|
+
contentClassName="flex min-h-10 shrink-0 flex-col gap-px rounded-lg pb-2 pt-1"
|
|
688
|
+
dndSensors={dndSensors}
|
|
689
|
+
emptyState={<SidebarPinnedEmptyState />}
|
|
690
|
+
label={s.pinned}
|
|
691
|
+
onArchiveSession={onArchiveSession}
|
|
692
|
+
onDeleteSession={onDeleteSession}
|
|
693
|
+
onReorder={handlePinnedDragEnd}
|
|
694
|
+
onResumeSession={onResumeSession}
|
|
695
|
+
onToggle={() => setSidebarPinsOpen(!pinsOpen)}
|
|
696
|
+
onTogglePin={unpinSession}
|
|
697
|
+
open={pinsOpen}
|
|
698
|
+
pinned
|
|
699
|
+
rootClassName="shrink-0 p-0 pb-1"
|
|
700
|
+
sessions={pinnedSessions}
|
|
701
|
+
sortable={pinnedSessions.length > 1}
|
|
702
|
+
workingSessionIdSet={workingSessionIdSet}
|
|
703
|
+
/>
|
|
704
|
+
)}
|
|
705
|
+
|
|
706
|
+
{sidebarOpen && showSessionSections && !trimmedQuery && (
|
|
707
|
+
<SidebarSessionsSection
|
|
708
|
+
activeSessionId={activeSidebarSessionId}
|
|
709
|
+
contentClassName={cn(
|
|
710
|
+
'flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-1.75',
|
|
711
|
+
// Separate profile sections clearly in the ALL view; rows inside
|
|
712
|
+
// each group keep their own tight gap-px rhythm.
|
|
713
|
+
showAllProfiles ? 'gap-3' : 'gap-px'
|
|
714
|
+
)}
|
|
715
|
+
dndSensors={dndSensors}
|
|
716
|
+
emptyState={showSessionSkeletons ? <SidebarSessionSkeletons /> : <SidebarAllPinnedState />}
|
|
717
|
+
footer={
|
|
718
|
+
// Hide "load more" only when workspace-grouped (those groups page
|
|
719
|
+
// themselves). ALL-profiles now pages per-profile from each profile
|
|
720
|
+
// header; the global footer only applies to non-ALL views.
|
|
721
|
+
!showAllProfiles && !agentsGrouped && !showSessionSkeletons && hasMoreSessions ? (
|
|
722
|
+
<SidebarLoadMoreRow
|
|
723
|
+
loading={sessionsLoading}
|
|
724
|
+
onClick={onLoadMoreSessions}
|
|
725
|
+
step={Math.min(SIDEBAR_SESSIONS_PAGE_SIZE, remainingSessionCount)}
|
|
726
|
+
/>
|
|
727
|
+
) : null
|
|
728
|
+
}
|
|
729
|
+
forceEmptyState={showSessionSkeletons}
|
|
730
|
+
groups={showAllProfiles ? profileGroups : agentsGrouped ? agentGroups : undefined}
|
|
731
|
+
headerAction={
|
|
732
|
+
// Always reserve the icon-xs (size-6) slot so the header keeps the
|
|
733
|
+
// same height whether or not the toggle renders — otherwise the
|
|
734
|
+
// "Sessions" label jumps when switching to the ALL-profiles view.
|
|
735
|
+
// Grouping operates on unpinned recents; if everything is pinned
|
|
736
|
+
// the toggle does nothing, and it's irrelevant in the ALL-profiles
|
|
737
|
+
// view (always grouped by profile), so hide the button (not the slot).
|
|
738
|
+
<div className="grid size-6 shrink-0 place-items-center">
|
|
739
|
+
{!showAllProfiles && agentSessions.length > 0 ? (
|
|
740
|
+
<Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>
|
|
741
|
+
<Button
|
|
742
|
+
aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}
|
|
743
|
+
className={cn(
|
|
744
|
+
'text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100',
|
|
745
|
+
agentsGrouped && 'bg-(--ui-control-active-background) text-foreground opacity-100'
|
|
746
|
+
)}
|
|
747
|
+
onClick={event => {
|
|
748
|
+
event.stopPropagation()
|
|
749
|
+
setSidebarRecentsOpen(true)
|
|
750
|
+
setSidebarAgentsGrouped(!agentsGrouped)
|
|
751
|
+
}}
|
|
752
|
+
size="icon-xs"
|
|
753
|
+
variant="ghost"
|
|
754
|
+
>
|
|
755
|
+
<Codicon name={agentsGrouped ? 'list-unordered' : 'root-folder'} size="0.75rem" />
|
|
756
|
+
</Button>
|
|
757
|
+
</Tip>
|
|
758
|
+
) : null}
|
|
759
|
+
</div>
|
|
760
|
+
}
|
|
761
|
+
label={s.sessions}
|
|
762
|
+
labelMeta={recentsMeta}
|
|
763
|
+
onArchiveSession={onArchiveSession}
|
|
764
|
+
onDeleteSession={onDeleteSession}
|
|
765
|
+
onNewSessionInWorkspace={showAllProfiles ? undefined : onNewSessionInWorkspace}
|
|
766
|
+
onReorder={showAllProfiles ? undefined : handleAgentDragEnd}
|
|
767
|
+
onResumeSession={onResumeSession}
|
|
768
|
+
onToggle={() => setSidebarRecentsOpen(!agentsOpen)}
|
|
769
|
+
onTogglePin={pinSession}
|
|
770
|
+
open={agentsOpen}
|
|
771
|
+
pinned={false}
|
|
772
|
+
rootClassName="min-h-0 flex-1 p-0"
|
|
773
|
+
sessions={agentSessions}
|
|
774
|
+
sortable={!showAllProfiles && agentSessions.length > 1}
|
|
775
|
+
workingSessionIdSet={workingSessionIdSet}
|
|
776
|
+
/>
|
|
777
|
+
)}
|
|
778
|
+
|
|
779
|
+
{sidebarOpen && !trimmedQuery && cronJobs.length > 0 && (
|
|
780
|
+
<SidebarCronJobsSection
|
|
781
|
+
jobs={cronJobs}
|
|
782
|
+
label={s.cronJobs}
|
|
783
|
+
onManageJob={onManageCronJob}
|
|
784
|
+
onOpenRun={onResumeSession}
|
|
785
|
+
onToggle={() => setSidebarCronOpen(!cronOpen)}
|
|
786
|
+
onTriggerJob={onTriggerCronJob}
|
|
787
|
+
open={cronOpen}
|
|
788
|
+
/>
|
|
789
|
+
)}
|
|
790
|
+
|
|
791
|
+
{sidebarOpen && !showSessionSections && <div className="min-h-0 flex-1" />}
|
|
792
|
+
|
|
793
|
+
{sidebarOpen && (
|
|
794
|
+
<div className="shrink-0 px-0.5 pb-1 pt-0.5">
|
|
795
|
+
<ProfileRail />
|
|
796
|
+
</div>
|
|
797
|
+
)}
|
|
798
|
+
</SidebarContent>
|
|
799
|
+
</Sidebar>
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
interface SidebarSectionHeaderProps {
|
|
804
|
+
label: string
|
|
805
|
+
open: boolean
|
|
806
|
+
onToggle: () => void
|
|
807
|
+
action?: React.ReactNode
|
|
808
|
+
meta?: React.ReactNode
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function SidebarSectionHeader({ label, open, onToggle, action, meta }: SidebarSectionHeaderProps) {
|
|
812
|
+
return (
|
|
813
|
+
<div className="group/section flex shrink-0 items-center justify-between pb-1 pt-1.5">
|
|
814
|
+
<button
|
|
815
|
+
className="group/section-label flex w-fit items-center gap-1 bg-transparent text-left leading-none"
|
|
816
|
+
onClick={onToggle}
|
|
817
|
+
type="button"
|
|
818
|
+
>
|
|
819
|
+
<SidebarPanelLabel>{label}</SidebarPanelLabel>
|
|
820
|
+
{meta && <SidebarCount>{meta}</SidebarCount>}
|
|
821
|
+
<DisclosureCaret
|
|
822
|
+
className="text-(--ui-text-tertiary) opacity-0 transition group-hover/section-label:opacity-100"
|
|
823
|
+
open={open}
|
|
824
|
+
/>
|
|
825
|
+
</button>
|
|
826
|
+
{action}
|
|
827
|
+
</div>
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function SidebarSessionSkeletons() {
|
|
832
|
+
return (
|
|
833
|
+
<div aria-hidden="true" className="grid gap-px">
|
|
834
|
+
{['w-32', 'w-40', 'w-28', 'w-36', 'w-24'].map((width, i) => (
|
|
835
|
+
<div className="grid min-h-7 grid-cols-[minmax(0,1fr)_1.5rem] items-center rounded-lg" key={`${width}-${i}`}>
|
|
836
|
+
<Skeleton className={cn('h-3.5 rounded-full', width)} />
|
|
837
|
+
<Skeleton className="mx-auto size-4 rounded-md opacity-60" />
|
|
838
|
+
</div>
|
|
839
|
+
))}
|
|
840
|
+
</div>
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function SidebarAllPinnedState() {
|
|
845
|
+
const { t } = useI18n()
|
|
846
|
+
|
|
847
|
+
return (
|
|
848
|
+
<div className="grid min-h-24 place-items-center rounded-lg text-center text-xs text-(--ui-text-tertiary)">
|
|
849
|
+
{t.sidebar.allPinned}
|
|
850
|
+
</div>
|
|
851
|
+
)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function SidebarPinnedEmptyState() {
|
|
855
|
+
const { t } = useI18n()
|
|
856
|
+
|
|
857
|
+
return (
|
|
858
|
+
<div className="flex min-h-7 items-center gap-1.5 rounded-lg pl-2 text-[0.75rem] text-(--ui-text-tertiary)">
|
|
859
|
+
<span className="grid w-3.5 shrink-0 place-items-center text-(--ui-text-quaternary)">
|
|
860
|
+
<Codicon name="pin" size="0.75rem" />
|
|
861
|
+
</span>
|
|
862
|
+
<span>{t.sidebar.shiftClickHint}</span>
|
|
863
|
+
</div>
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
interface SidebarSessionGroup {
|
|
868
|
+
id: string
|
|
869
|
+
label: string
|
|
870
|
+
path: null | string
|
|
871
|
+
sessions: SessionInfo[]
|
|
872
|
+
// Profile color for the ALL-profiles view; absent for workspace groups.
|
|
873
|
+
color?: null | string
|
|
874
|
+
loadingMore?: boolean
|
|
875
|
+
mode?: 'profile' | 'workspace'
|
|
876
|
+
onLoadMore?: () => void
|
|
877
|
+
totalCount?: number
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
interface SidebarSessionsSectionProps {
|
|
881
|
+
label: string
|
|
882
|
+
open: boolean
|
|
883
|
+
onToggle: () => void
|
|
884
|
+
sessions: SessionInfo[]
|
|
885
|
+
activeSessionId: null | string
|
|
886
|
+
workingSessionIdSet: Set<string>
|
|
887
|
+
onResumeSession: (sessionId: string) => void
|
|
888
|
+
onDeleteSession: (sessionId: string) => void
|
|
889
|
+
onArchiveSession: (sessionId: string) => void
|
|
890
|
+
onTogglePin: (sessionId: string) => void
|
|
891
|
+
onNewSessionInWorkspace?: (path: null | string) => void
|
|
892
|
+
pinned: boolean
|
|
893
|
+
rootClassName?: string
|
|
894
|
+
contentClassName?: string
|
|
895
|
+
emptyState: React.ReactNode
|
|
896
|
+
forceEmptyState?: boolean
|
|
897
|
+
headerAction?: React.ReactNode
|
|
898
|
+
footer?: React.ReactNode
|
|
899
|
+
groups?: SidebarSessionGroup[]
|
|
900
|
+
labelMeta?: React.ReactNode
|
|
901
|
+
sortable?: boolean
|
|
902
|
+
onReorder?: (event: DragEndEvent) => void
|
|
903
|
+
dndSensors?: ReturnType<typeof useSensors>
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function SidebarSessionsSection({
|
|
907
|
+
label,
|
|
908
|
+
open,
|
|
909
|
+
onToggle,
|
|
910
|
+
sessions,
|
|
911
|
+
activeSessionId,
|
|
912
|
+
workingSessionIdSet,
|
|
913
|
+
onResumeSession,
|
|
914
|
+
onDeleteSession,
|
|
915
|
+
onArchiveSession,
|
|
916
|
+
onTogglePin,
|
|
917
|
+
onNewSessionInWorkspace,
|
|
918
|
+
pinned,
|
|
919
|
+
rootClassName,
|
|
920
|
+
contentClassName,
|
|
921
|
+
emptyState,
|
|
922
|
+
forceEmptyState = false,
|
|
923
|
+
headerAction,
|
|
924
|
+
footer,
|
|
925
|
+
groups,
|
|
926
|
+
labelMeta,
|
|
927
|
+
sortable = false,
|
|
928
|
+
onReorder,
|
|
929
|
+
dndSensors
|
|
930
|
+
}: SidebarSessionsSectionProps) {
|
|
931
|
+
const showEmptyState = forceEmptyState || sessions.length === 0
|
|
932
|
+
const dndActive = sortable && !!onReorder
|
|
933
|
+
|
|
934
|
+
const renderRow = (session: SessionInfo) => {
|
|
935
|
+
const rowProps = {
|
|
936
|
+
isPinned: pinned,
|
|
937
|
+
isSelected: session.id === activeSessionId,
|
|
938
|
+
isWorking: workingSessionIdSet.has(session.id),
|
|
939
|
+
onArchive: () => onArchiveSession(session.id),
|
|
940
|
+
onDelete: () => onDeleteSession(session.id),
|
|
941
|
+
onPin: () => onTogglePin(sessionPinId(session)),
|
|
942
|
+
onResume: () => onResumeSession(session.id),
|
|
943
|
+
session
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return sortable ? (
|
|
947
|
+
<SortableSidebarSessionRow key={session.id} {...rowProps} />
|
|
948
|
+
) : (
|
|
949
|
+
<SidebarSessionRow key={session.id} {...rowProps} />
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const renderRows = (items: SessionInfo[]) => items.map(renderRow)
|
|
954
|
+
|
|
955
|
+
const renderSessionList = (items: SessionInfo[]) =>
|
|
956
|
+
dndActive ? (
|
|
957
|
+
<SortableContext items={items.map(s => s.id)} strategy={verticalListSortingStrategy}>
|
|
958
|
+
{renderRows(items)}
|
|
959
|
+
</SortableContext>
|
|
960
|
+
) : (
|
|
961
|
+
renderRows(items)
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
const flatVirtualized = !showEmptyState && !groups?.length && sessions.length >= VIRTUALIZE_THRESHOLD
|
|
965
|
+
|
|
966
|
+
let inner: React.ReactNode
|
|
967
|
+
|
|
968
|
+
if (showEmptyState) {
|
|
969
|
+
inner = emptyState
|
|
970
|
+
} else if (groups?.length) {
|
|
971
|
+
const groupNodes = groups.map(group =>
|
|
972
|
+
dndActive ? (
|
|
973
|
+
<SortableSidebarWorkspaceGroup
|
|
974
|
+
group={group}
|
|
975
|
+
key={group.id}
|
|
976
|
+
onNewSession={onNewSessionInWorkspace}
|
|
977
|
+
renderRows={renderSessionList}
|
|
978
|
+
/>
|
|
979
|
+
) : (
|
|
980
|
+
<SidebarWorkspaceGroup
|
|
981
|
+
group={group}
|
|
982
|
+
key={group.id}
|
|
983
|
+
onNewSession={onNewSessionInWorkspace}
|
|
984
|
+
renderRows={renderSessionList}
|
|
985
|
+
/>
|
|
986
|
+
)
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
inner = dndActive ? (
|
|
990
|
+
<SortableContext items={groups.map(g => wsId(g.id))} strategy={verticalListSortingStrategy}>
|
|
991
|
+
{groupNodes}
|
|
992
|
+
</SortableContext>
|
|
993
|
+
) : (
|
|
994
|
+
groupNodes
|
|
995
|
+
)
|
|
996
|
+
} else if (flatVirtualized) {
|
|
997
|
+
inner = (
|
|
998
|
+
<VirtualSessionList
|
|
999
|
+
activeSessionId={activeSessionId}
|
|
1000
|
+
onArchiveSession={onArchiveSession}
|
|
1001
|
+
onDeleteSession={onDeleteSession}
|
|
1002
|
+
onResumeSession={onResumeSession}
|
|
1003
|
+
onTogglePin={onTogglePin}
|
|
1004
|
+
pinned={pinned}
|
|
1005
|
+
sessions={sessions}
|
|
1006
|
+
sortable={sortable}
|
|
1007
|
+
workingSessionIdSet={workingSessionIdSet}
|
|
1008
|
+
/>
|
|
1009
|
+
)
|
|
1010
|
+
} else {
|
|
1011
|
+
inner = renderSessionList(sessions)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const body =
|
|
1015
|
+
dndActive && !showEmptyState ? (
|
|
1016
|
+
<DndContext collisionDetection={closestCenter} onDragEnd={onReorder} sensors={dndSensors}>
|
|
1017
|
+
{inner}
|
|
1018
|
+
</DndContext>
|
|
1019
|
+
) : (
|
|
1020
|
+
inner
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
// The virtualizer owns its own scroller, so suppress the wrapper's overflow
|
|
1024
|
+
// to avoid a double scroll container.
|
|
1025
|
+
const resolvedContentClassName = cn(contentClassName, flatVirtualized && 'overflow-y-visible')
|
|
1026
|
+
|
|
1027
|
+
return (
|
|
1028
|
+
<SidebarGroup className={rootClassName}>
|
|
1029
|
+
<SidebarSectionHeader action={headerAction} label={label} meta={labelMeta} onToggle={onToggle} open={open} />
|
|
1030
|
+
{open && (
|
|
1031
|
+
<SidebarGroupContent className={resolvedContentClassName}>
|
|
1032
|
+
{body}
|
|
1033
|
+
{footer}
|
|
1034
|
+
</SidebarGroupContent>
|
|
1035
|
+
)}
|
|
1036
|
+
</SidebarGroup>
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
interface SidebarWorkspaceGroupProps extends React.ComponentProps<'div'> {
|
|
1041
|
+
group: SidebarSessionGroup
|
|
1042
|
+
renderRows: (sessions: SessionInfo[]) => React.ReactNode
|
|
1043
|
+
onNewSession?: (path: null | string) => void
|
|
1044
|
+
reorderable?: boolean
|
|
1045
|
+
dragging?: boolean
|
|
1046
|
+
dragHandleProps?: React.HTMLAttributes<HTMLElement>
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function SidebarWorkspaceGroup({
|
|
1050
|
+
group,
|
|
1051
|
+
renderRows,
|
|
1052
|
+
onNewSession,
|
|
1053
|
+
reorderable = false,
|
|
1054
|
+
dragging = false,
|
|
1055
|
+
dragHandleProps,
|
|
1056
|
+
className,
|
|
1057
|
+
style,
|
|
1058
|
+
ref,
|
|
1059
|
+
...rest
|
|
1060
|
+
}: SidebarWorkspaceGroupProps) {
|
|
1061
|
+
const { t } = useI18n()
|
|
1062
|
+
const s = t.sidebar
|
|
1063
|
+
const isProfileGroup = group.mode === 'profile'
|
|
1064
|
+
const pageStep = isProfileGroup ? PROFILE_INITIAL_PAGE : WORKSPACE_PAGE
|
|
1065
|
+
const [open, setOpen] = useState(true)
|
|
1066
|
+
const [visibleCount, setVisibleCount] = useState(pageStep)
|
|
1067
|
+
|
|
1068
|
+
const loadedCount = group.sessions.length
|
|
1069
|
+
// Profile groups know their on-disk total (children excluded); workspace
|
|
1070
|
+
// groups only ever page within what's already loaded.
|
|
1071
|
+
const totalCount = isProfileGroup ? Math.max(group.totalCount ?? loadedCount, loadedCount) : loadedCount
|
|
1072
|
+
const visibleSessions = group.sessions.slice(0, visibleCount)
|
|
1073
|
+
const hiddenCount = Math.max(0, totalCount - visibleSessions.length)
|
|
1074
|
+
const nextCount = Math.min(pageStep, hiddenCount)
|
|
1075
|
+
|
|
1076
|
+
// Reveal already-loaded rows first; only hit the backend when the next page
|
|
1077
|
+
// crosses what's been fetched for this profile.
|
|
1078
|
+
const handleProfileLoadMore = () => {
|
|
1079
|
+
const target = visibleCount + pageStep
|
|
1080
|
+
|
|
1081
|
+
setVisibleCount(target)
|
|
1082
|
+
|
|
1083
|
+
if (target > loadedCount && loadedCount < totalCount) {
|
|
1084
|
+
group.onLoadMore?.()
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return (
|
|
1089
|
+
<div className={cn('grid gap-px', dragging && 'z-10 opacity-60', className)} ref={ref} style={style} {...rest}>
|
|
1090
|
+
<div className="group/workspace flex min-h-6 items-center gap-1 px-2 pt-1 text-[0.6875rem] font-medium text-(--ui-text-tertiary)">
|
|
1091
|
+
<button
|
|
1092
|
+
className="flex min-w-0 items-center gap-1.5 bg-transparent text-left hover:text-(--ui-text-secondary)"
|
|
1093
|
+
onClick={() => setOpen(value => !value)}
|
|
1094
|
+
type="button"
|
|
1095
|
+
>
|
|
1096
|
+
{group.color ? (
|
|
1097
|
+
<span aria-hidden="true" className="size-2 shrink-0 rounded-full" style={{ backgroundColor: group.color }} />
|
|
1098
|
+
) : null}
|
|
1099
|
+
<span className="truncate">{group.label}</span>
|
|
1100
|
+
<SidebarCount>
|
|
1101
|
+
{isProfileGroup ? countLabel(visibleSessions.length, totalCount) : group.sessions.length}
|
|
1102
|
+
</SidebarCount>
|
|
1103
|
+
<DisclosureCaret
|
|
1104
|
+
className="text-(--ui-text-tertiary) opacity-0 transition group-hover/workspace:opacity-100"
|
|
1105
|
+
open={open}
|
|
1106
|
+
/>
|
|
1107
|
+
</button>
|
|
1108
|
+
{(onNewSession || isProfileGroup) && (
|
|
1109
|
+
<Tip label={s.newSessionIn(group.label)}>
|
|
1110
|
+
<button
|
|
1111
|
+
aria-label={s.newSessionIn(group.label)}
|
|
1112
|
+
className="grid size-4 shrink-0 place-items-center rounded-sm bg-transparent text-(--ui-text-quaternary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground group-hover/workspace:opacity-100"
|
|
1113
|
+
// Profile groups start a fresh session in that profile but keep the
|
|
1114
|
+
// all-profiles browse view (newSessionInProfile leaves the scope
|
|
1115
|
+
// alone); workspace groups seed the new session's cwd from the path.
|
|
1116
|
+
onClick={() => (isProfileGroup ? newSessionInProfile(group.id) : onNewSession?.(group.path))}
|
|
1117
|
+
type="button"
|
|
1118
|
+
>
|
|
1119
|
+
<Codicon name="add" size="0.75rem" />
|
|
1120
|
+
</button>
|
|
1121
|
+
</Tip>
|
|
1122
|
+
)}
|
|
1123
|
+
{reorderable && (
|
|
1124
|
+
<span
|
|
1125
|
+
{...dragHandleProps}
|
|
1126
|
+
aria-label={s.reorderWorkspace(group.label)}
|
|
1127
|
+
className="ml-auto -my-0.5 grid w-4 shrink-0 cursor-grab touch-none place-items-center self-stretch overflow-hidden active:cursor-grabbing"
|
|
1128
|
+
onClick={event => event.stopPropagation()}
|
|
1129
|
+
>
|
|
1130
|
+
<Codicon
|
|
1131
|
+
className={cn(
|
|
1132
|
+
'text-(--ui-text-quaternary) opacity-0 transition-opacity group-hover/workspace:opacity-80 hover:text-(--ui-text-secondary)',
|
|
1133
|
+
dragging && 'text-(--ui-text-secondary) opacity-100'
|
|
1134
|
+
)}
|
|
1135
|
+
name="grabber"
|
|
1136
|
+
size="0.75rem"
|
|
1137
|
+
/>
|
|
1138
|
+
</span>
|
|
1139
|
+
)}
|
|
1140
|
+
</div>
|
|
1141
|
+
{open && (
|
|
1142
|
+
<>
|
|
1143
|
+
{renderRows(visibleSessions)}
|
|
1144
|
+
{hiddenCount > 0 &&
|
|
1145
|
+
(isProfileGroup ? (
|
|
1146
|
+
<SidebarLoadMoreRow loading={Boolean(group.loadingMore)} onClick={handleProfileLoadMore} step={nextCount} />
|
|
1147
|
+
) : (
|
|
1148
|
+
<Tip label={s.showMoreIn(nextCount, group.label)}>
|
|
1149
|
+
<button
|
|
1150
|
+
aria-label={s.showMoreIn(nextCount, group.label)}
|
|
1151
|
+
className="ml-auto grid size-5 place-items-center rounded-sm bg-transparent text-(--ui-text-tertiary) transition-colors hover:bg-(--ui-control-hover-background) hover:text-foreground"
|
|
1152
|
+
onClick={() => setVisibleCount(count => count + WORKSPACE_PAGE)}
|
|
1153
|
+
type="button"
|
|
1154
|
+
>
|
|
1155
|
+
<Codicon name="ellipsis" size="0.75rem" />
|
|
1156
|
+
</button>
|
|
1157
|
+
</Tip>
|
|
1158
|
+
))}
|
|
1159
|
+
</>
|
|
1160
|
+
)}
|
|
1161
|
+
</div>
|
|
1162
|
+
)
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
interface SortableWorkspaceProps {
|
|
1166
|
+
group: SidebarSessionGroup
|
|
1167
|
+
renderRows: (sessions: SessionInfo[]) => React.ReactNode
|
|
1168
|
+
onNewSession?: (path: null | string) => void
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function SortableSidebarWorkspaceGroup(props: SortableWorkspaceProps) {
|
|
1172
|
+
return <SidebarWorkspaceGroup {...props} {...useSortableBindings(wsId(props.group.id))} />
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function SidebarCount({ children }: { children: React.ReactNode }) {
|
|
1176
|
+
return <span className="text-[0.6875rem] font-medium text-(--ui-text-quaternary)">{children}</span>
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
interface SortableSessionRowProps {
|
|
1180
|
+
session: SessionInfo
|
|
1181
|
+
isPinned: boolean
|
|
1182
|
+
isSelected: boolean
|
|
1183
|
+
isWorking: boolean
|
|
1184
|
+
onArchive: () => void
|
|
1185
|
+
onDelete: () => void
|
|
1186
|
+
onPin: () => void
|
|
1187
|
+
onResume: () => void
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function SortableSidebarSessionRow(props: SortableSessionRowProps) {
|
|
1191
|
+
return <SidebarSessionRow {...props} {...useSortableBindings(props.session.id)} />
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
interface SidebarLoadMoreRowProps {
|
|
1195
|
+
loading: boolean
|
|
1196
|
+
onClick: () => void
|
|
1197
|
+
step: number
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function SidebarLoadMoreRow({ loading, onClick, step }: SidebarLoadMoreRowProps) {
|
|
1201
|
+
const { t } = useI18n()
|
|
1202
|
+
const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore
|
|
1203
|
+
|
|
1204
|
+
return (
|
|
1205
|
+
<button
|
|
1206
|
+
className="flex min-h-5 items-center gap-1.5 self-start bg-transparent pl-2 text-left text-[0.6875rem] text-(--ui-text-tertiary) transition-colors duration-100 ease-out hover:text-foreground hover:transition-none disabled:cursor-default disabled:opacity-60 disabled:hover:text-(--ui-text-tertiary)"
|
|
1207
|
+
disabled={loading}
|
|
1208
|
+
onClick={onClick}
|
|
1209
|
+
type="button"
|
|
1210
|
+
>
|
|
1211
|
+
{/* Seat the icon in the same w-3.5 column session rows use for their dot
|
|
1212
|
+
so the chevron + label line up with the rows above. */}
|
|
1213
|
+
<span className="grid w-3.5 shrink-0 place-items-center">
|
|
1214
|
+
<Codicon className="opacity-70" name={loading ? 'loading' : 'chevron-down'} size="0.75rem" spinning={loading} />
|
|
1215
|
+
</span>
|
|
1216
|
+
<span>{label}</span>
|
|
1217
|
+
</button>
|
|
1218
|
+
)
|
|
1219
|
+
}
|