@clawpump/claw-agent 0.1.5 → 0.1.7
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/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2177 -3162
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +215 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6190 -973
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +4 -1
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +300 -684
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
|
@@ -1,36 +1,89 @@
|
|
|
1
1
|
import type { AppendMessage, ThreadMessage } from '@assistant-ui/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useStore } from '@nanostores/react'
|
|
3
|
+
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
|
3
4
|
|
|
4
|
-
import { transcribeAudio } from '@/hermes'
|
|
5
|
-
import {
|
|
5
|
+
import { getProfiles, transcribeAudio } from '@/hermes'
|
|
6
|
+
import { translateNow, type Translations, useI18n } from '@/i18n'
|
|
7
|
+
import { stripAnsi } from '@/lib/ansi'
|
|
8
|
+
import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
|
|
6
9
|
import {
|
|
7
|
-
|
|
8
|
-
INTERRUPTED_MARKER,
|
|
10
|
+
optimisticAttachmentRef,
|
|
9
11
|
parseCommandDispatch,
|
|
10
12
|
parseSlashCommand,
|
|
11
13
|
pathLabel,
|
|
14
|
+
sessionTitle,
|
|
12
15
|
SLASH_COMMAND_RE
|
|
13
16
|
} from '@/lib/chat-runtime'
|
|
14
17
|
import {
|
|
15
18
|
type CommandsCatalogLike,
|
|
19
|
+
type DesktopActionId,
|
|
20
|
+
type DesktopPickerId,
|
|
16
21
|
desktopSlashUnavailableMessage,
|
|
17
22
|
filterDesktopCommandsCatalog,
|
|
18
|
-
isDesktopSlashCommand
|
|
23
|
+
isDesktopSlashCommand,
|
|
24
|
+
resolveDesktopCommand
|
|
19
25
|
} from '@/lib/desktop-slash-commands'
|
|
20
26
|
import { triggerHaptic } from '@/lib/haptics'
|
|
27
|
+
import { setMutableRef } from '@/lib/mutable-ref'
|
|
21
28
|
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
|
|
29
|
+
import { setSessionYolo } from '@/lib/yolo-session'
|
|
22
30
|
import {
|
|
23
31
|
$composerAttachments,
|
|
24
|
-
addComposerAttachment,
|
|
25
32
|
clearComposerAttachments,
|
|
26
33
|
type ComposerAttachment,
|
|
27
|
-
|
|
34
|
+
setComposerAttachmentUploadState,
|
|
35
|
+
setComposerDraft,
|
|
36
|
+
terminalContextBlocksFromDraft,
|
|
37
|
+
updateComposerAttachment
|
|
28
38
|
} from '@/store/composer'
|
|
39
|
+
import { resetSessionBackground } from '@/store/composer-status'
|
|
29
40
|
import { clearNotifications, notify, notifyError } from '@/store/notifications'
|
|
30
41
|
import { requestDesktopOnboarding } from '@/store/onboarding'
|
|
31
|
-
import { $
|
|
42
|
+
import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
|
|
43
|
+
import {
|
|
44
|
+
$busy,
|
|
45
|
+
$connection,
|
|
46
|
+
$messages,
|
|
47
|
+
$sessions,
|
|
48
|
+
$yoloActive,
|
|
49
|
+
setAwaitingResponse,
|
|
50
|
+
setBusy,
|
|
51
|
+
setMessages,
|
|
52
|
+
setModelPickerOpen,
|
|
53
|
+
setSessionPickerOpen,
|
|
54
|
+
setSessions,
|
|
55
|
+
setYoloActive
|
|
56
|
+
} from '@/store/session'
|
|
57
|
+
import { clearSessionSubagents } from '@/store/subagents'
|
|
58
|
+
import { clearSessionTodos } from '@/store/todos'
|
|
59
|
+
|
|
60
|
+
import type {
|
|
61
|
+
ClientSessionState,
|
|
62
|
+
BrowserManageResponse,
|
|
63
|
+
FileAttachResponse,
|
|
64
|
+
HandoffFailResponse,
|
|
65
|
+
HandoffRequestResponse,
|
|
66
|
+
HandoffStateResponse,
|
|
67
|
+
ImageAttachResponse,
|
|
68
|
+
SessionSteerResponse,
|
|
69
|
+
SessionTitleResponse,
|
|
70
|
+
SlashExecResponse
|
|
71
|
+
} from '../../types'
|
|
72
|
+
|
|
73
|
+
interface HandoffResult {
|
|
74
|
+
ok: boolean
|
|
75
|
+
error?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function delay(ms: number): Promise<void> {
|
|
79
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
80
|
+
}
|
|
32
81
|
|
|
33
|
-
|
|
82
|
+
function isSessionIdCandidate(value: string): boolean {
|
|
83
|
+
const trimmed = value.trim()
|
|
84
|
+
|
|
85
|
+
return /^\d{8}_\d{6}_[A-Fa-f0-9]{6}$/.test(trimmed) || /^[A-Fa-f0-9]{32}$/.test(trimmed)
|
|
86
|
+
}
|
|
34
87
|
|
|
35
88
|
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
36
89
|
return new Promise((resolve, reject) => {
|
|
@@ -40,10 +93,10 @@ function blobToDataUrl(blob: Blob): Promise<string> {
|
|
|
40
93
|
if (typeof reader.result === 'string') {
|
|
41
94
|
resolve(reader.result)
|
|
42
95
|
} else {
|
|
43
|
-
reject(new Error('
|
|
96
|
+
reject(new Error(translateNow('desktop.audioReadFailed')))
|
|
44
97
|
}
|
|
45
98
|
})
|
|
46
|
-
reader.addEventListener('error', () => reject(reader.error || new Error('
|
|
99
|
+
reader.addEventListener('error', () => reject(reader.error || new Error(translateNow('desktop.audioReadFailed'))))
|
|
47
100
|
reader.readAsDataURL(blob)
|
|
48
101
|
})
|
|
49
102
|
}
|
|
@@ -60,14 +113,208 @@ function inlineErrorMessage(error: unknown, fallback: string): string {
|
|
|
60
113
|
return (raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw).replace(/^Error:\s*/, '').trim()
|
|
61
114
|
}
|
|
62
115
|
|
|
116
|
+
function isSessionNotFoundError(error: unknown): boolean {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
118
|
+
|
|
119
|
+
return /session not found/i.test(message)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The gateway refuses prompt.submit while a turn is running (4009 "session
|
|
123
|
+
// busy"). It's a transient concurrency guard, never a user-facing error: a
|
|
124
|
+
// submit racing the settle edge (or a rewind interrupting mid-turn) just waits
|
|
125
|
+
// a beat for the turn to wind down, then lands. Bounded so a genuinely stuck
|
|
126
|
+
// turn still surfaces eventually.
|
|
127
|
+
const SESSION_BUSY_RETRY_TIMEOUT_MS = 6_000
|
|
128
|
+
const SESSION_BUSY_RETRY_INTERVAL_MS = 150
|
|
129
|
+
|
|
130
|
+
function isSessionBusyError(error: unknown): boolean {
|
|
131
|
+
return /session busy/i.test(error instanceof Error ? error.message : String(error))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
135
|
+
|
|
136
|
+
// Retry a gateway call across transient "session busy" so it never reaches the
|
|
137
|
+
// user — the turn settles within the deadline and the call lands.
|
|
138
|
+
async function withSessionBusyRetry<T>(call: () => Promise<T>): Promise<T> {
|
|
139
|
+
const deadline = Date.now() + SESSION_BUSY_RETRY_TIMEOUT_MS
|
|
140
|
+
|
|
141
|
+
for (;;) {
|
|
142
|
+
try {
|
|
143
|
+
return await call()
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (isSessionBusyError(err) && Date.now() < deadline) {
|
|
146
|
+
await sleep(SESSION_BUSY_RETRY_INTERVAL_MS)
|
|
147
|
+
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function base64FromDataUrl(dataUrl: string): string {
|
|
157
|
+
const comma = dataUrl.indexOf(',')
|
|
158
|
+
|
|
159
|
+
return comma >= 0 ? dataUrl.slice(comma + 1) : ''
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function imageFilenameFromPath(filePath: string): string {
|
|
163
|
+
return filePath.split(/[\\/]/).filter(Boolean).pop() || 'image.png'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Remote gateway: the local composer-image file lives on THIS machine's disk,
|
|
167
|
+
// not the gateway's, so read the bytes here and upload them via
|
|
168
|
+
// image.attach_bytes. Returns null when the file can't be read.
|
|
169
|
+
async function readImageForRemoteAttach(
|
|
170
|
+
filePath: string
|
|
171
|
+
): Promise<{ contentBase64: string; filename: string } | null> {
|
|
172
|
+
const dataUrl = await window.hermesDesktop?.readFileDataUrl(filePath)
|
|
173
|
+
const contentBase64 = dataUrl ? base64FromDataUrl(dataUrl) : ''
|
|
174
|
+
|
|
175
|
+
return contentBase64 ? { contentBase64, filename: imageFilenameFromPath(filePath) } : null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Read a non-image file as a data URL for upload via file.attach. Returns null
|
|
179
|
+
// when the desktop bridge can't read the file (e.g. it was moved/deleted).
|
|
180
|
+
async function readFileDataUrlForAttach(filePath: string): Promise<string | null> {
|
|
181
|
+
const reader = window.hermesDesktop?.readFileDataUrl
|
|
182
|
+
|
|
183
|
+
if (!reader) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const dataUrl = await reader(filePath)
|
|
188
|
+
|
|
189
|
+
return dataUrl || null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The readFileDataUrl IPC base64-loads the whole file into memory and is
|
|
193
|
+
// hard-capped (DATA_URL_READ_MAX_BYTES, 16 MB) in electron/hardening.cjs, which
|
|
194
|
+
// rejects with a raw "file is too large (N bytes; limit M bytes)" string. In
|
|
195
|
+
// remote mode every attachment's bytes go through that read, so a big file
|
|
196
|
+
// surfaces that internal message verbatim in the failure toast. Translate it
|
|
197
|
+
// into a friendly "too large to upload to the remote gateway" line, parsing the
|
|
198
|
+
// limit out of the message so it tracks the real cap. Non-cap errors pass
|
|
199
|
+
// through unchanged.
|
|
200
|
+
function friendlyRemoteAttachError(err: unknown, label: string): Error {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
202
|
+
|
|
203
|
+
if (!/too large/i.test(message)) {
|
|
204
|
+
return err instanceof Error ? err : new Error(message)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const limitBytes = Number(message.match(/limit (\d+) bytes/)?.[1])
|
|
208
|
+
const cap = Number.isFinite(limitBytes) && limitBytes > 0 ? ` (max ${Math.floor(limitBytes / (1024 * 1024))} MB)` : ''
|
|
209
|
+
|
|
210
|
+
return new Error(`${label} is too large to upload to the remote gateway${cap}.`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type GatewayRequest = <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stage one file/image attachment into the session workspace and return the
|
|
217
|
+
* attachment rewritten with the gateway-side ref. Images upload their bytes in
|
|
218
|
+
* remote mode (so vision works) and pass the path locally; non-image files
|
|
219
|
+
* upload bytes remotely and pass the path locally. Throws on failure so callers
|
|
220
|
+
* can surface an error. Shared by submit-time sync, the eager drop-time upload,
|
|
221
|
+
* and the message-edit composer drop — keep them in lockstep.
|
|
222
|
+
*/
|
|
223
|
+
export async function uploadComposerAttachment(
|
|
224
|
+
attachment: ComposerAttachment,
|
|
225
|
+
opts: { remote: boolean; requestGateway: GatewayRequest; sessionId: string }
|
|
226
|
+
): Promise<ComposerAttachment> {
|
|
227
|
+
const { remote, requestGateway, sessionId } = opts
|
|
228
|
+
const path = attachment.path ?? ''
|
|
229
|
+
const label = attachment.label || pathLabel(path)
|
|
230
|
+
|
|
231
|
+
if (attachment.kind === 'image') {
|
|
232
|
+
let result: ImageAttachResponse
|
|
233
|
+
|
|
234
|
+
if (remote) {
|
|
235
|
+
let payload: Awaited<ReturnType<typeof readImageForRemoteAttach>>
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
payload = await readImageForRemoteAttach(path)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
throw friendlyRemoteAttachError(err, label)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!payload) {
|
|
244
|
+
throw new Error(`Could not read ${label}`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
result = await requestGateway<ImageAttachResponse>('image.attach_bytes', {
|
|
248
|
+
session_id: sessionId,
|
|
249
|
+
content_base64: payload.contentBase64,
|
|
250
|
+
filename: payload.filename
|
|
251
|
+
})
|
|
252
|
+
} else {
|
|
253
|
+
result = await requestGateway<ImageAttachResponse>('image.attach', {
|
|
254
|
+
path,
|
|
255
|
+
session_id: sessionId
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!result.attached) {
|
|
260
|
+
throw new Error(result.message || `Could not attach ${label}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const attachedPath = result.path || path
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...attachment,
|
|
267
|
+
attachedSessionId: sessionId,
|
|
268
|
+
label: attachedPath ? pathLabel(attachedPath) : attachment.label,
|
|
269
|
+
path: attachedPath,
|
|
270
|
+
uploadState: undefined
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Non-image file.
|
|
275
|
+
let dataUrl: string | null = null
|
|
276
|
+
|
|
277
|
+
if (remote) {
|
|
278
|
+
try {
|
|
279
|
+
dataUrl = await readFileDataUrlForAttach(path)
|
|
280
|
+
} catch (err) {
|
|
281
|
+
throw friendlyRemoteAttachError(err, label)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!dataUrl) {
|
|
285
|
+
throw new Error(`Could not read ${label}`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = await requestGateway<FileAttachResponse>('file.attach', {
|
|
290
|
+
name: label,
|
|
291
|
+
path,
|
|
292
|
+
session_id: sessionId,
|
|
293
|
+
...(dataUrl ? { data_url: dataUrl } : {})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (!result.attached || !result.ref_text) {
|
|
297
|
+
throw new Error(result.message || `Could not attach ${label}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
...attachment,
|
|
302
|
+
attachedSessionId: sessionId,
|
|
303
|
+
refText: result.ref_text,
|
|
304
|
+
uploadState: undefined
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
63
308
|
interface PromptActionsOptions {
|
|
64
309
|
activeSessionId: string | null
|
|
65
310
|
activeSessionIdRef: MutableRefObject<string | null>
|
|
66
311
|
busyRef: MutableRefObject<boolean>
|
|
67
312
|
branchCurrentSession: () => Promise<boolean>
|
|
68
|
-
createBackendSessionForSend: () => Promise<string | null>
|
|
313
|
+
createBackendSessionForSend: (preview?: string | null) => Promise<string | null>
|
|
69
314
|
handleSkinCommand: (arg: string) => string
|
|
315
|
+
refreshSessions: () => Promise<void>
|
|
70
316
|
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
317
|
+
resumeStoredSession: (storedSessionId: string) => Promise<void> | void
|
|
71
318
|
selectedStoredSessionIdRef: MutableRefObject<string | null>
|
|
72
319
|
startFreshSessionDraft: () => void
|
|
73
320
|
sttEnabled: boolean
|
|
@@ -83,12 +330,21 @@ interface SubmitTextOptions {
|
|
|
83
330
|
fromQueue?: boolean
|
|
84
331
|
}
|
|
85
332
|
|
|
86
|
-
|
|
333
|
+
/** Everything a slash handler needs about the invocation it's serving. */
|
|
334
|
+
interface SlashActionCtx {
|
|
335
|
+
arg: string
|
|
336
|
+
command: string
|
|
337
|
+
name: string
|
|
338
|
+
recordInput: boolean
|
|
339
|
+
sessionHint?: string
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderCommandsCatalog(catalog: CommandsCatalogLike, copy: Translations['desktop']): string {
|
|
87
343
|
const desktopCatalog = filterDesktopCommandsCatalog(catalog)
|
|
88
344
|
|
|
89
345
|
const sections = desktopCatalog.categories?.length
|
|
90
346
|
? desktopCatalog.categories
|
|
91
|
-
: [{ name:
|
|
347
|
+
: [{ name: copy.desktopCommands, pairs: desktopCatalog.pairs ?? [] }]
|
|
92
348
|
|
|
93
349
|
const body = sections
|
|
94
350
|
.filter(section => section.pairs.length > 0)
|
|
@@ -100,8 +356,8 @@ function renderCommandsCatalog(catalog: CommandsCatalogLike): string {
|
|
|
100
356
|
.join('\n\n')
|
|
101
357
|
|
|
102
358
|
const tail = [
|
|
103
|
-
desktopCatalog.skill_count ?
|
|
104
|
-
desktopCatalog.warning ?
|
|
359
|
+
desktopCatalog.skill_count ? copy.skillCommandsAvailable(desktopCatalog.skill_count) : '',
|
|
360
|
+
desktopCatalog.warning ? copy.warningLine(desktopCatalog.warning) : ''
|
|
105
361
|
]
|
|
106
362
|
.filter(Boolean)
|
|
107
363
|
.join('\n')
|
|
@@ -131,15 +387,24 @@ export function usePromptActions({
|
|
|
131
387
|
branchCurrentSession,
|
|
132
388
|
createBackendSessionForSend,
|
|
133
389
|
handleSkinCommand,
|
|
390
|
+
refreshSessions,
|
|
134
391
|
requestGateway,
|
|
392
|
+
resumeStoredSession,
|
|
135
393
|
selectedStoredSessionIdRef,
|
|
136
394
|
startFreshSessionDraft,
|
|
137
395
|
sttEnabled,
|
|
138
396
|
updateSessionState
|
|
139
397
|
}: PromptActionsOptions) {
|
|
398
|
+
const { t } = useI18n()
|
|
399
|
+
const copy = t.desktop
|
|
400
|
+
|
|
140
401
|
const appendSessionTextMessage = useCallback(
|
|
141
402
|
(sessionId: string, role: ChatMessage['role'], text: string) => {
|
|
142
|
-
|
|
403
|
+
// Strip ANSI: slash-command output from the backend worker carries SGR
|
|
404
|
+
// color codes (e.g. "Unknown command" in red). The ESC byte is invisible
|
|
405
|
+
// in the chat panel, so without this the `[1;31m…[0m` payload leaks as
|
|
406
|
+
// literal text.
|
|
407
|
+
const body = stripAnsi(text).trim()
|
|
143
408
|
|
|
144
409
|
if (!body) {
|
|
145
410
|
return
|
|
@@ -164,80 +429,173 @@ export function usePromptActions({
|
|
|
164
429
|
[selectedStoredSessionIdRef, updateSessionState]
|
|
165
430
|
)
|
|
166
431
|
|
|
167
|
-
|
|
432
|
+
// In-flight drop-time eager uploads, keyed by attachment id. Submit joins
|
|
433
|
+
// these before re-uploading so a drop-then-immediately-Enter can't fire
|
|
434
|
+
// file.attach twice and stage duplicate copies on the gateway.
|
|
435
|
+
const eagerUploadInFlight = useRef<Map<string, Promise<void>>>(new Map())
|
|
436
|
+
|
|
437
|
+
const syncAttachmentsForSubmit = useCallback(
|
|
168
438
|
async (
|
|
169
439
|
sessionId: string,
|
|
170
440
|
attachments: ComposerAttachment[],
|
|
171
441
|
options: { updateComposerAttachments?: boolean } = {}
|
|
172
|
-
) => {
|
|
442
|
+
): Promise<ComposerAttachment[]> => {
|
|
173
443
|
const updateComposerAttachments = options.updateComposerAttachments ?? true
|
|
174
|
-
const
|
|
444
|
+
const remote = $connection.get()?.mode === 'remote'
|
|
445
|
+
const synced: ComposerAttachment[] = []
|
|
446
|
+
|
|
447
|
+
for (const original of attachments) {
|
|
448
|
+
let attachment = original
|
|
449
|
+
|
|
450
|
+
// Join a drop-time eager upload still in flight for this attachment
|
|
451
|
+
// before deciding anything — otherwise submit and the eager task both
|
|
452
|
+
// call file.attach and stage duplicate files. After it settles, take the
|
|
453
|
+
// store's updated copy (its gateway ref, or its failure) over the stale
|
|
454
|
+
// pre-upload snapshot.
|
|
455
|
+
const inFlight = eagerUploadInFlight.current.get(attachment.id)
|
|
456
|
+
|
|
457
|
+
if (inFlight) {
|
|
458
|
+
await inFlight
|
|
459
|
+
attachment = $composerAttachments.get().find(item => item.id === attachment.id) ?? attachment
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Already-synced or pathless refs (terminal, url, etc.) pass through.
|
|
463
|
+
// A drop-time eager upload may already have staged this one (matching
|
|
464
|
+
// attachedSessionId) — don't re-upload it.
|
|
465
|
+
if (!attachment.path || attachment.attachedSessionId === sessionId) {
|
|
466
|
+
synced.push(attachment)
|
|
175
467
|
|
|
176
|
-
for (const attachment of images) {
|
|
177
|
-
if (attachment.attachedSessionId === sessionId) {
|
|
178
468
|
continue
|
|
179
469
|
}
|
|
180
470
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
path: attachment.path
|
|
184
|
-
})
|
|
471
|
+
if (attachment.kind === 'image' || attachment.kind === 'file') {
|
|
472
|
+
const nextAttachment = await uploadComposerAttachment(attachment, { remote, requestGateway, sessionId })
|
|
185
473
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
474
|
+
// Update-only: never resurrect a chip the user removed mid-upload.
|
|
475
|
+
if (updateComposerAttachments) {
|
|
476
|
+
updateComposerAttachment(nextAttachment)
|
|
477
|
+
}
|
|
190
478
|
|
|
191
|
-
|
|
479
|
+
synced.push(nextAttachment)
|
|
192
480
|
|
|
193
|
-
|
|
194
|
-
addComposerAttachment({
|
|
195
|
-
...attachment,
|
|
196
|
-
id: attachment.id,
|
|
197
|
-
label: attachedPath ? pathLabel(attachedPath) : attachment.label,
|
|
198
|
-
path: attachedPath,
|
|
199
|
-
attachedSessionId: sessionId
|
|
200
|
-
})
|
|
481
|
+
continue
|
|
201
482
|
}
|
|
483
|
+
|
|
484
|
+
synced.push(attachment)
|
|
202
485
|
}
|
|
486
|
+
|
|
487
|
+
return synced
|
|
203
488
|
},
|
|
204
489
|
[requestGateway]
|
|
205
490
|
)
|
|
206
491
|
|
|
492
|
+
// Stage a freshly dropped file as soon as it lands (when a session already
|
|
493
|
+
// exists), so the upload runs while the user is still typing rather than
|
|
494
|
+
// stalling the send. The card shows a spinner via `uploadState`; on success
|
|
495
|
+
// the chip carries its gateway-side ref so submit skips re-uploading.
|
|
496
|
+
//
|
|
497
|
+
// Images are intentionally NOT eager-uploaded: attachImagePath adds the chip
|
|
498
|
+
// and then fills in `previewUrl` (the base64 thumbnail) on a second tick, so
|
|
499
|
+
// an eager upload would race that write — clobbering the thumbnail and
|
|
500
|
+
// swapping `path` to a gateway path the local preview can't read. Images are
|
|
501
|
+
// small and still byte-upload at submit via image.attach_bytes.
|
|
502
|
+
const eagerlyUploadAttachment = useCallback(
|
|
503
|
+
async (sessionId: string, attachment: ComposerAttachment) => {
|
|
504
|
+
const remote = $connection.get()?.mode === 'remote'
|
|
505
|
+
|
|
506
|
+
setComposerAttachmentUploadState(attachment.id, 'uploading')
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
// Update-only: if the user removed the chip while this was uploading,
|
|
510
|
+
// don't resurrect it — just drop the staged result on the floor.
|
|
511
|
+
updateComposerAttachment(await uploadComposerAttachment(attachment, { remote, requestGateway, sessionId }))
|
|
512
|
+
} catch (err) {
|
|
513
|
+
// Leave the chip in place so submit-time sync can retry (or the user can
|
|
514
|
+
// remove it) and flag the card; also toast so a hard failure (unreadable
|
|
515
|
+
// file, gateway perms) isn't swallowed while the user keeps typing.
|
|
516
|
+
setComposerAttachmentUploadState(attachment.id, 'error')
|
|
517
|
+
notifyError(err, copy.dropFiles)
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
[copy.dropFiles, requestGateway]
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
const composerAttachments = useStore($composerAttachments)
|
|
524
|
+
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
if (!activeSessionId) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (const attachment of composerAttachments) {
|
|
531
|
+
const needsUpload =
|
|
532
|
+
attachment.kind === 'file' &&
|
|
533
|
+
Boolean(attachment.path) &&
|
|
534
|
+
!attachment.attachedSessionId &&
|
|
535
|
+
!attachment.uploadState &&
|
|
536
|
+
!eagerUploadInFlight.current.has(attachment.id)
|
|
537
|
+
|
|
538
|
+
if (!needsUpload) {
|
|
539
|
+
continue
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const task = eagerlyUploadAttachment(activeSessionId, attachment).finally(() =>
|
|
543
|
+
eagerUploadInFlight.current.delete(attachment.id)
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
eagerUploadInFlight.current.set(attachment.id, task)
|
|
547
|
+
}
|
|
548
|
+
}, [activeSessionId, composerAttachments, eagerlyUploadAttachment])
|
|
549
|
+
|
|
207
550
|
const submitPromptText = useCallback(
|
|
208
551
|
async (rawText: string, options?: SubmitTextOptions) => {
|
|
209
552
|
const visibleText = rawText.trim()
|
|
210
553
|
const usingComposerAttachments = !options?.attachments
|
|
211
554
|
const attachments = options?.attachments ?? $composerAttachments.get()
|
|
212
555
|
|
|
213
|
-
const contextRefs = attachments
|
|
214
|
-
.map(a => a.refText)
|
|
215
|
-
.filter(Boolean)
|
|
216
|
-
.join('\n')
|
|
217
|
-
|
|
218
556
|
const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n')
|
|
219
557
|
const hasImage = attachments.some(a => a.kind === 'image')
|
|
220
|
-
const attachmentRefs = attachments.map(attachmentDisplayText).filter((r): r is string => Boolean(r))
|
|
221
558
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
559
|
+
// Refs are recomputed after sync (file.attach rewrites @file: refs to
|
|
560
|
+
// workspace-relative paths the remote gateway can resolve). Seed the
|
|
561
|
+
// optimistic message with the pre-sync refs, then rewrite once synced.
|
|
562
|
+
// Images use their base64 preview so the thumbnail renders inline without
|
|
563
|
+
// a (remote-mode 403-prone) /api/media fetch — see optimisticAttachmentRef.
|
|
564
|
+
let attachmentRefs = attachments.map(optimisticAttachmentRef).filter((r): r is string => Boolean(r))
|
|
565
|
+
|
|
566
|
+
const buildContextText = (atts: ComposerAttachment[]): string => {
|
|
567
|
+
const contextRefs = atts
|
|
568
|
+
.map(a => a.refText)
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.join('\n')
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
[contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
|
|
574
|
+
(atts.some(a => a.kind === 'image') ? 'What do you see in this image?' : '')
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Queue drains fire on the busy→false settle edge, where busyRef (synced
|
|
579
|
+
// from $busy by a separate effect) may still read true — honoring it would
|
|
580
|
+
// bounce the drained send. The drain lock serializes them; the user path
|
|
581
|
+
// keeps the guard so a stray Enter mid-turn can't double-submit.
|
|
582
|
+
const hasSendable = Boolean(visibleText || terminalContextBlocks || attachments.length || hasImage)
|
|
225
583
|
|
|
226
|
-
if (!
|
|
584
|
+
if (!hasSendable || (!options?.fromQueue && busyRef.current)) {
|
|
227
585
|
return false
|
|
228
586
|
}
|
|
229
587
|
|
|
230
588
|
const optimisticId = `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
231
589
|
|
|
232
|
-
const
|
|
590
|
+
const buildUserMessage = (): ChatMessage => ({
|
|
233
591
|
id: optimisticId,
|
|
234
592
|
role: 'user',
|
|
235
593
|
parts: [textPart(visibleText || (attachmentRefs.length ? '' : attachments.map(a => a.label).join(', ')))],
|
|
236
594
|
attachmentRefs
|
|
237
|
-
}
|
|
595
|
+
})
|
|
238
596
|
|
|
239
597
|
const releaseBusy = () => {
|
|
240
|
-
busyRef
|
|
598
|
+
setMutableRef(busyRef, false)
|
|
241
599
|
setBusy(false)
|
|
242
600
|
setAwaitingResponse(false)
|
|
243
601
|
}
|
|
@@ -251,12 +609,27 @@ export function usePromptActions({
|
|
|
251
609
|
...state,
|
|
252
610
|
messages: state.messages.some(m => m.id === optimisticId)
|
|
253
611
|
? state.messages
|
|
254
|
-
: [...state.messages,
|
|
612
|
+
: [...state.messages, buildUserMessage()],
|
|
255
613
|
busy: true,
|
|
256
614
|
awaitingResponse: true,
|
|
257
615
|
pendingBranchGroup: null,
|
|
258
616
|
sawAssistantPayload: false,
|
|
259
|
-
|
|
617
|
+
// Fresh submit = new turn — clear any leftover interrupt flag, else
|
|
618
|
+
// mutateStream/completeAssistantMessage drop every delta of this turn
|
|
619
|
+
// (what made drained-after-interrupt sends go silent).
|
|
620
|
+
interrupted: false
|
|
621
|
+
}),
|
|
622
|
+
selectedStoredSessionIdRef.current
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// After sync rewrites refs, refresh the optimistic message in place so the
|
|
626
|
+
// transcript shows the resolved @file: ref rather than the local path.
|
|
627
|
+
const rewriteOptimistic = (sid: string) =>
|
|
628
|
+
updateSessionState(
|
|
629
|
+
sid,
|
|
630
|
+
state => ({
|
|
631
|
+
...state,
|
|
632
|
+
messages: state.messages.map(message => (message.id === optimisticId ? buildUserMessage() : message))
|
|
260
633
|
}),
|
|
261
634
|
selectedStoredSessionIdRef.current
|
|
262
635
|
)
|
|
@@ -281,7 +654,7 @@ export function usePromptActions({
|
|
|
281
654
|
)
|
|
282
655
|
}
|
|
283
656
|
|
|
284
|
-
busyRef
|
|
657
|
+
setMutableRef(busyRef, true)
|
|
285
658
|
setBusy(true)
|
|
286
659
|
setAwaitingResponse(true)
|
|
287
660
|
clearNotifications()
|
|
@@ -291,16 +664,16 @@ export function usePromptActions({
|
|
|
291
664
|
if (sessionId) {
|
|
292
665
|
seedOptimistic(sessionId)
|
|
293
666
|
} else {
|
|
294
|
-
setMessages(current => [...current,
|
|
667
|
+
setMessages(current => [...current, buildUserMessage()])
|
|
295
668
|
}
|
|
296
669
|
|
|
297
670
|
if (!sessionId) {
|
|
298
671
|
try {
|
|
299
|
-
sessionId = await createBackendSessionForSend()
|
|
672
|
+
sessionId = await createBackendSessionForSend(visibleText)
|
|
300
673
|
} catch (err) {
|
|
301
674
|
dropOptimistic(null)
|
|
302
675
|
releaseBusy()
|
|
303
|
-
notifyError(err,
|
|
676
|
+
notifyError(err, copy.sessionUnavailable)
|
|
304
677
|
|
|
305
678
|
return false
|
|
306
679
|
}
|
|
@@ -308,7 +681,7 @@ export function usePromptActions({
|
|
|
308
681
|
if (!sessionId) {
|
|
309
682
|
dropOptimistic(null)
|
|
310
683
|
releaseBusy()
|
|
311
|
-
notify({ kind: 'error', title:
|
|
684
|
+
notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
|
|
312
685
|
|
|
313
686
|
return false
|
|
314
687
|
}
|
|
@@ -317,10 +690,47 @@ export function usePromptActions({
|
|
|
317
690
|
}
|
|
318
691
|
|
|
319
692
|
try {
|
|
320
|
-
await
|
|
693
|
+
const syncedAttachments = await syncAttachmentsForSubmit(sessionId, attachments, {
|
|
321
694
|
updateComposerAttachments: usingComposerAttachments
|
|
322
695
|
})
|
|
323
|
-
|
|
696
|
+
|
|
697
|
+
// Rewrite the optimistic message + prompt text with the synced refs so
|
|
698
|
+
// the gateway receives @file: paths that resolve in its workspace.
|
|
699
|
+
// (Images keep their inline base64 preview — see optimisticAttachmentRef.)
|
|
700
|
+
attachmentRefs = syncedAttachments.map(optimisticAttachmentRef).filter((r): r is string => Boolean(r))
|
|
701
|
+
rewriteOptimistic(sessionId)
|
|
702
|
+
const text = buildContextText(syncedAttachments)
|
|
703
|
+
|
|
704
|
+
// On sleep/wake the gateway's in-memory session may have been cleared
|
|
705
|
+
// while the desktop app still holds the old session ID. Detect this,
|
|
706
|
+
// resume the stored session to re-register it, and retry once.
|
|
707
|
+
let submitErr: unknown = null
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
await withSessionBusyRetry(() => requestGateway('prompt.submit', { session_id: sessionId, text }))
|
|
711
|
+
} catch (firstErr) {
|
|
712
|
+
if (isSessionNotFoundError(firstErr) && selectedStoredSessionIdRef.current) {
|
|
713
|
+
// Re-register the session in the gateway and get a fresh live ID.
|
|
714
|
+
const resumed = await requestGateway<{ session_id: string }>('session.resume', {
|
|
715
|
+
session_id: selectedStoredSessionIdRef.current
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
const recoveredId = resumed?.session_id
|
|
719
|
+
|
|
720
|
+
if (recoveredId) {
|
|
721
|
+
activeSessionIdRef.current = recoveredId
|
|
722
|
+
await withSessionBusyRetry(() => requestGateway('prompt.submit', { session_id: recoveredId, text }))
|
|
723
|
+
} else {
|
|
724
|
+
submitErr = firstErr
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
submitErr = firstErr
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (submitErr !== null) {
|
|
732
|
+
throw submitErr
|
|
733
|
+
}
|
|
324
734
|
|
|
325
735
|
if (usingComposerAttachments) {
|
|
326
736
|
clearComposerAttachments()
|
|
@@ -328,9 +738,17 @@ export function usePromptActions({
|
|
|
328
738
|
|
|
329
739
|
return true
|
|
330
740
|
} catch (err) {
|
|
331
|
-
const message = inlineErrorMessage(err, 'Prompt failed')
|
|
332
|
-
|
|
333
741
|
releaseBusy()
|
|
742
|
+
|
|
743
|
+
// A queued drain that raced a not-yet-settled turn gets a transient
|
|
744
|
+
// "session busy" (4009). Don't surface an error bubble/toast — the entry
|
|
745
|
+
// stays queued and the composer's bounded auto-drain retries when idle.
|
|
746
|
+
if (options?.fromQueue && isSessionBusyError(err)) {
|
|
747
|
+
return false
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const message = inlineErrorMessage(err, copy.promptFailed)
|
|
751
|
+
|
|
334
752
|
updateSessionState(sessionId, state => ({
|
|
335
753
|
...state,
|
|
336
754
|
messages: [
|
|
@@ -339,7 +757,7 @@ export function usePromptActions({
|
|
|
339
757
|
id: `assistant-error-${Date.now()}`,
|
|
340
758
|
role: 'assistant',
|
|
341
759
|
parts: [],
|
|
342
|
-
error: message ||
|
|
760
|
+
error: message || copy.promptFailed,
|
|
343
761
|
branchGroupId: state.pendingBranchGroup ?? undefined
|
|
344
762
|
}
|
|
345
763
|
],
|
|
@@ -350,12 +768,12 @@ export function usePromptActions({
|
|
|
350
768
|
}))
|
|
351
769
|
|
|
352
770
|
if (isProviderSetupError(err)) {
|
|
353
|
-
requestDesktopOnboarding(
|
|
771
|
+
requestDesktopOnboarding(copy.providerCredentialRequired)
|
|
354
772
|
|
|
355
773
|
return false
|
|
356
774
|
}
|
|
357
775
|
|
|
358
|
-
notifyError(err,
|
|
776
|
+
notifyError(err, copy.promptFailed)
|
|
359
777
|
|
|
360
778
|
return false
|
|
361
779
|
}
|
|
@@ -363,82 +781,134 @@ export function usePromptActions({
|
|
|
363
781
|
[
|
|
364
782
|
activeSessionId,
|
|
365
783
|
busyRef,
|
|
784
|
+
copy,
|
|
366
785
|
createBackendSessionForSend,
|
|
367
786
|
requestGateway,
|
|
368
787
|
selectedStoredSessionIdRef,
|
|
369
|
-
|
|
788
|
+
syncAttachmentsForSubmit,
|
|
370
789
|
updateSessionState
|
|
371
790
|
]
|
|
372
791
|
)
|
|
373
792
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
793
|
+
// Queue a handoff of this session to a messaging platform and watch it to
|
|
794
|
+
// a terminal state. We only write the request through the gateway; the
|
|
795
|
+
// separate `hermes gateway` process performs the actual transfer, so we
|
|
796
|
+
// poll `handoff.state` (mirror of the CLI's block-poll) for the result.
|
|
797
|
+
const handoffSession = useCallback(
|
|
798
|
+
async (
|
|
799
|
+
platform: string,
|
|
800
|
+
options?: { onProgress?: (state: string) => void; sessionId?: string }
|
|
801
|
+
): Promise<HandoffResult> => {
|
|
802
|
+
const sid = options?.sessionId || activeSessionIdRef.current
|
|
380
803
|
|
|
381
|
-
|
|
382
|
-
|
|
804
|
+
if (!sid) {
|
|
805
|
+
return { error: copy.sessionUnavailable, ok: false }
|
|
806
|
+
}
|
|
383
807
|
|
|
384
|
-
|
|
385
|
-
appendSessionTextMessage(sessionId, 'system', 'empty slash command')
|
|
386
|
-
}
|
|
808
|
+
const target = platform.trim().toLowerCase()
|
|
387
809
|
|
|
388
|
-
|
|
389
|
-
}
|
|
810
|
+
if (!target) {
|
|
811
|
+
return { error: copy.handoff.failed(''), ok: false }
|
|
812
|
+
}
|
|
390
813
|
|
|
391
|
-
|
|
392
|
-
|
|
814
|
+
try {
|
|
815
|
+
options?.onProgress?.('pending')
|
|
816
|
+
await requestGateway<HandoffRequestResponse>('handoff.request', {
|
|
817
|
+
platform: target,
|
|
818
|
+
session_id: sid
|
|
819
|
+
})
|
|
820
|
+
} catch (err) {
|
|
821
|
+
return { error: inlineErrorMessage(err, copy.handoff.failed(target)), ok: false }
|
|
822
|
+
}
|
|
393
823
|
|
|
394
|
-
|
|
395
|
-
|
|
824
|
+
const deadline = Date.now() + 60_000
|
|
825
|
+
let lastState = 'pending'
|
|
396
826
|
|
|
397
|
-
|
|
398
|
-
|
|
827
|
+
while (Date.now() < deadline) {
|
|
828
|
+
await delay(800)
|
|
399
829
|
|
|
400
|
-
|
|
830
|
+
let record: HandoffStateResponse
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
record = await requestGateway<HandoffStateResponse>('handoff.state', { session_id: sid })
|
|
834
|
+
} catch {
|
|
835
|
+
continue
|
|
401
836
|
}
|
|
402
837
|
|
|
403
|
-
|
|
404
|
-
notify({ kind: 'success', message: handleSkinCommand(arg) })
|
|
838
|
+
const state = record.state || 'pending'
|
|
405
839
|
|
|
406
|
-
|
|
840
|
+
if (state !== lastState) {
|
|
841
|
+
options?.onProgress?.(state)
|
|
842
|
+
lastState = state
|
|
407
843
|
}
|
|
408
844
|
|
|
409
|
-
|
|
845
|
+
if (state === 'completed') {
|
|
846
|
+
appendSessionTextMessage(sid, 'system', copy.handoff.systemNote(target))
|
|
847
|
+
notify({ kind: 'success', message: copy.handoff.success(target) })
|
|
410
848
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
kind: 'error',
|
|
414
|
-
title: 'Session unavailable',
|
|
415
|
-
message: 'Could not create a new session'
|
|
416
|
-
})
|
|
849
|
+
return { ok: true }
|
|
850
|
+
}
|
|
417
851
|
|
|
418
|
-
|
|
852
|
+
if (state === 'failed') {
|
|
853
|
+
return { error: record.error || copy.handoff.failed(target), ok: false }
|
|
419
854
|
}
|
|
855
|
+
}
|
|
420
856
|
|
|
421
|
-
|
|
422
|
-
|
|
857
|
+
const cleanup = await requestGateway<HandoffFailResponse>('handoff.fail', {
|
|
858
|
+
error: copy.handoff.timedOut,
|
|
859
|
+
session_id: sid
|
|
860
|
+
}).catch(() => null)
|
|
423
861
|
|
|
424
|
-
|
|
425
|
-
|
|
862
|
+
if (cleanup?.state === 'completed') {
|
|
863
|
+
appendSessionTextMessage(sid, 'system', copy.handoff.systemNote(target))
|
|
864
|
+
notify({ kind: 'success', message: copy.handoff.success(target) })
|
|
426
865
|
|
|
427
|
-
|
|
866
|
+
return { ok: true }
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return { error: copy.handoff.timedOut, ok: false }
|
|
870
|
+
},
|
|
871
|
+
[activeSessionIdRef, appendSessionTextMessage, copy, requestGateway]
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
const executeSlashCommand = useCallback(
|
|
875
|
+
async (rawCommand: string, options?: { sessionId?: string; recordInput?: boolean }) => {
|
|
876
|
+
const ensureSessionId = async (sessionHint?: string) =>
|
|
877
|
+
sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
|
|
878
|
+
|
|
879
|
+
// Resolve the target session plus a writer for inline slash output, or
|
|
880
|
+
// notify + return null when none can be created. Folds the ensure / bail /
|
|
881
|
+
// build-renderSlashOutput boilerplate every exec-style handler repeats.
|
|
882
|
+
const withSlashOutput = async (
|
|
883
|
+
ctx: SlashActionCtx
|
|
884
|
+
): Promise<{ render: (text: string) => void; sessionId: string } | null> => {
|
|
885
|
+
const sessionId = await ensureSessionId(ctx.sessionHint)
|
|
886
|
+
|
|
887
|
+
if (!sessionId) {
|
|
888
|
+
notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
|
|
889
|
+
|
|
890
|
+
return null
|
|
428
891
|
}
|
|
429
892
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
|
|
893
|
+
const render = (text: string) =>
|
|
894
|
+
appendSessionTextMessage(sessionId, 'system', ctx.recordInput ? slashStatusText(ctx.command, text) : text)
|
|
433
895
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
896
|
+
return { render, sessionId }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// `exec` commands (and unknown skill / quick commands the backend owns)
|
|
900
|
+
// run on the gateway and render their text output inline. This is the only
|
|
901
|
+
// path that talks to slash.exec / command.dispatch.
|
|
902
|
+
async function runExec(ctx: SlashActionCtx): Promise<void> {
|
|
903
|
+
const { arg, command, name } = ctx
|
|
904
|
+
const resolved = await withSlashOutput(ctx)
|
|
438
905
|
|
|
906
|
+
if (!resolved) {
|
|
439
907
|
return
|
|
440
908
|
}
|
|
441
909
|
|
|
910
|
+
const { render: renderSlashOutput, sessionId } = resolved
|
|
911
|
+
|
|
442
912
|
if (!isDesktopSlashCommand(name)) {
|
|
443
913
|
renderSlashOutput(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
|
|
444
914
|
|
|
@@ -461,11 +931,7 @@ export function usePromptActions({
|
|
|
461
931
|
|
|
462
932
|
try {
|
|
463
933
|
const dispatch = parseCommandDispatch(
|
|
464
|
-
await requestGateway<unknown>('command.dispatch', {
|
|
465
|
-
session_id: sessionId,
|
|
466
|
-
name,
|
|
467
|
-
arg
|
|
468
|
-
})
|
|
934
|
+
await requestGateway<unknown>('command.dispatch', { session_id: sessionId, name, arg })
|
|
469
935
|
)
|
|
470
936
|
|
|
471
937
|
if (!dispatch) {
|
|
@@ -486,8 +952,26 @@ export function usePromptActions({
|
|
|
486
952
|
return
|
|
487
953
|
}
|
|
488
954
|
|
|
955
|
+
// send / prefill carry an optional `notice` (e.g. "⊙ Goal set …")
|
|
956
|
+
// that the backend wants shown as a system line before the message
|
|
957
|
+
// is acted on. Mirrors the TUI's createSlashHandler — without it a
|
|
958
|
+
// `/goal <text>` looked like it did nothing.
|
|
959
|
+
if ((dispatch.type === 'send' || dispatch.type === 'prefill') && dispatch.notice?.trim()) {
|
|
960
|
+
renderSlashOutput(dispatch.notice.trim())
|
|
961
|
+
}
|
|
962
|
+
|
|
489
963
|
const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
|
|
490
964
|
|
|
965
|
+
// /undo returns a prefill directive: drop the backed-up message into
|
|
966
|
+
// the composer for editing instead of submitting it immediately.
|
|
967
|
+
if (dispatch.type === 'prefill') {
|
|
968
|
+
if (message) {
|
|
969
|
+
setComposerDraft(message)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
|
|
491
975
|
if (!message) {
|
|
492
976
|
renderSlashOutput(
|
|
493
977
|
`/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}`
|
|
@@ -512,6 +996,336 @@ export function usePromptActions({
|
|
|
512
996
|
}
|
|
513
997
|
}
|
|
514
998
|
|
|
999
|
+
// One handler per `action` command. Adding a desktop-native command is a
|
|
1000
|
+
// registry row in desktop-slash-commands.ts plus an entry here — never a
|
|
1001
|
+
// new branch in a dispatch ladder.
|
|
1002
|
+
const actionHandlers: Record<DesktopActionId, (ctx: SlashActionCtx) => Promise<void>> = {
|
|
1003
|
+
new: async () => {
|
|
1004
|
+
startFreshSessionDraft()
|
|
1005
|
+
},
|
|
1006
|
+
branch: async () => {
|
|
1007
|
+
await branchCurrentSession()
|
|
1008
|
+
},
|
|
1009
|
+
// /yolo maps to the status-bar YOLO control — a per-session approval
|
|
1010
|
+
// bypass, same scope as the TUI's Shift+Tab. With no session yet we arm
|
|
1011
|
+
// it locally; the session-create path applies it on the first message.
|
|
1012
|
+
yolo: async ({ sessionHint }) => {
|
|
1013
|
+
const sid = sessionHint || activeSessionIdRef.current
|
|
1014
|
+
const next = !$yoloActive.get()
|
|
1015
|
+
|
|
1016
|
+
if (!sid) {
|
|
1017
|
+
setYoloActive(next)
|
|
1018
|
+
notify({ kind: 'success', message: next ? copy.yoloArmed : copy.yoloOff })
|
|
1019
|
+
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
try {
|
|
1024
|
+
const active = await setSessionYolo(requestGateway, sid, next)
|
|
1025
|
+
appendSessionTextMessage(sid, 'system', copy.yoloSystem(active))
|
|
1026
|
+
} catch {
|
|
1027
|
+
notify({ kind: 'error', title: copy.yoloTitle, message: copy.yoloToggleFailed })
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
// /handoff hands this session to a messaging platform. The platform is
|
|
1031
|
+
// completed inline in the slash popover (backend _handoff_completions),
|
|
1032
|
+
// so there is no overlay: `/handoff <platform>` runs the desktop's own
|
|
1033
|
+
// handoff RPC. cli_only on the backend, so it must not reach slash.exec.
|
|
1034
|
+
handoff: async ({ arg, command, recordInput, sessionHint }) => {
|
|
1035
|
+
const platform = arg.trim()
|
|
1036
|
+
|
|
1037
|
+
if (!platform) {
|
|
1038
|
+
notify({ kind: 'success', message: copy.handoff.pickPlatform })
|
|
1039
|
+
|
|
1040
|
+
return
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const sid = sessionHint || activeSessionIdRef.current
|
|
1044
|
+
|
|
1045
|
+
if (!sid) {
|
|
1046
|
+
notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
|
|
1047
|
+
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const result = await handoffSession(platform, { sessionId: sid })
|
|
1052
|
+
|
|
1053
|
+
if (!result.ok && result.error) {
|
|
1054
|
+
appendSessionTextMessage(sid, 'system', recordInput ? slashStatusText(command, result.error) : result.error)
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
// /profile selects which profile new chats open in — no app relaunch.
|
|
1058
|
+
// A profile is per-session now, so an existing thread can't change its
|
|
1059
|
+
// profile mid-stream; `/profile <name>` points the next new chat (and
|
|
1060
|
+
// the current empty draft) at that profile's backend.
|
|
1061
|
+
profile: async ({ arg }) => {
|
|
1062
|
+
const target = arg.trim()
|
|
1063
|
+
const current = normalizeProfileKey($activeGatewayProfile.get())
|
|
1064
|
+
|
|
1065
|
+
if (!target) {
|
|
1066
|
+
notify({ kind: 'success', message: copy.profileStatus(current) })
|
|
1067
|
+
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
const { profiles } = await getProfiles()
|
|
1073
|
+
const match = profiles.find(profile => profile.name === target)
|
|
1074
|
+
|
|
1075
|
+
if (!match) {
|
|
1076
|
+
notify({
|
|
1077
|
+
kind: 'error',
|
|
1078
|
+
title: copy.unknownProfile,
|
|
1079
|
+
message: copy.noProfileNamed(target, profiles.map(profile => profile.name).join(', '))
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
return
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const key = normalizeProfileKey(match.name)
|
|
1086
|
+
|
|
1087
|
+
$newChatProfile.set(key)
|
|
1088
|
+
await ensureGatewayProfile(key)
|
|
1089
|
+
notify({ kind: 'success', message: copy.newChatsProfile(match.name) })
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
notifyError(err, copy.setProfileFailed)
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
skin: async ({ arg, command, recordInput, sessionHint }) => {
|
|
1095
|
+
const sid = sessionHint || activeSessionIdRef.current
|
|
1096
|
+
const message = handleSkinCommand(arg)
|
|
1097
|
+
|
|
1098
|
+
// No session to print into yet — surface it as a toast instead of
|
|
1099
|
+
// spinning up a backend session just to change the theme.
|
|
1100
|
+
if (!sid) {
|
|
1101
|
+
notify({ kind: 'success', message })
|
|
1102
|
+
|
|
1103
|
+
return
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
appendSessionTextMessage(sid, 'system', recordInput ? slashStatusText(command, message) : message)
|
|
1107
|
+
},
|
|
1108
|
+
// /title <name> renames via the gateway's session.title RPC — the same
|
|
1109
|
+
// path the TUI uses, NOT REST renameSession (which 404s on runtime ids)
|
|
1110
|
+
// nor the slash worker (whose DB write can silently fail). Bare /title
|
|
1111
|
+
// shows the current title, which the worker owns, so delegate to exec.
|
|
1112
|
+
title: async ctx => {
|
|
1113
|
+
if (!ctx.arg) {
|
|
1114
|
+
await runExec(ctx)
|
|
1115
|
+
|
|
1116
|
+
return
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const resolved = await withSlashOutput(ctx)
|
|
1120
|
+
|
|
1121
|
+
if (!resolved) {
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const { render: renderSlashOutput, sessionId } = resolved
|
|
1126
|
+
const { arg } = ctx
|
|
1127
|
+
|
|
1128
|
+
try {
|
|
1129
|
+
const result = await requestGateway<SessionTitleResponse>('session.title', {
|
|
1130
|
+
session_id: sessionId,
|
|
1131
|
+
title: arg
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
const finalTitle = (result?.title || arg).trim()
|
|
1135
|
+
const queued = result?.pending === true
|
|
1136
|
+
|
|
1137
|
+
setSessions(prev => prev.map(s => (s.id === sessionId ? { ...s, title: finalTitle || null } : s)))
|
|
1138
|
+
await refreshSessions().catch(() => undefined)
|
|
1139
|
+
renderSlashOutput(
|
|
1140
|
+
finalTitle
|
|
1141
|
+
? `Session title set: ${finalTitle}${queued ? ' (queued while session initializes)' : ''}`
|
|
1142
|
+
: 'Session title cleared.'
|
|
1143
|
+
)
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
help: async ctx => {
|
|
1149
|
+
const resolved = await withSlashOutput(ctx)
|
|
1150
|
+
|
|
1151
|
+
if (!resolved) {
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const { render: renderSlashOutput, sessionId } = resolved
|
|
1156
|
+
|
|
1157
|
+
try {
|
|
1158
|
+
const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
|
|
1159
|
+
|
|
1160
|
+
renderSlashOutput(renderCommandsCatalog(catalog, copy))
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
// /browser connect|disconnect|status manages the live CDP connection on
|
|
1166
|
+
// the gateway host, mirroring the TUI's browser.manage RPC. It mutates
|
|
1167
|
+
// BROWSER_CDP_URL (and may launch Chrome) in the gateway process — only
|
|
1168
|
+
// meaningful when that process runs on this machine, so it's gated to
|
|
1169
|
+
// local connections. A remote gateway would act on the wrong host.
|
|
1170
|
+
browser: async ctx => {
|
|
1171
|
+
const resolved = await withSlashOutput(ctx)
|
|
1172
|
+
|
|
1173
|
+
if (!resolved) {
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const { render: renderSlashOutput, sessionId } = resolved
|
|
1178
|
+
|
|
1179
|
+
if ($connection.get()?.mode === 'remote') {
|
|
1180
|
+
renderSlashOutput(
|
|
1181
|
+
'/browser manages a Chromium-family browser on the gateway host — only available when connected to a local gateway.'
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
return
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const [rawAction = 'status', ...rest] = ctx.arg.trim().split(/\s+/).filter(Boolean)
|
|
1188
|
+
const cmdAction = rawAction.toLowerCase()
|
|
1189
|
+
|
|
1190
|
+
if (!['connect', 'disconnect', 'status'].includes(cmdAction)) {
|
|
1191
|
+
renderSlashOutput(
|
|
1192
|
+
'usage: /browser [connect|disconnect|status] [url] · persistent: set browser.cdp_url in config.yaml'
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
return
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const url = cmdAction === 'connect' ? rest.join(' ').trim() || 'http://127.0.0.1:9222' : undefined
|
|
1199
|
+
|
|
1200
|
+
if (url) {
|
|
1201
|
+
renderSlashOutput(`checking Chromium-family browser remote debugging at ${url}...`)
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const result = await requestGateway<BrowserManageResponse>('browser.manage', {
|
|
1206
|
+
action: cmdAction,
|
|
1207
|
+
session_id: sessionId,
|
|
1208
|
+
...(url && { url })
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
// Without a streamed session subscription, the gateway bundles its
|
|
1212
|
+
// progress lines into `messages` — flush them inline.
|
|
1213
|
+
result?.messages?.forEach(message => renderSlashOutput(message))
|
|
1214
|
+
|
|
1215
|
+
if (cmdAction === 'status') {
|
|
1216
|
+
renderSlashOutput(
|
|
1217
|
+
result?.connected
|
|
1218
|
+
? `browser connected: ${result.url || '(url unavailable)'}`
|
|
1219
|
+
: 'browser not connected (try /browser connect <url> or set browser.cdp_url in config.yaml)'
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (cmdAction === 'disconnect') {
|
|
1226
|
+
renderSlashOutput('browser disconnected')
|
|
1227
|
+
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (result?.connected) {
|
|
1232
|
+
renderSlashOutput('Browser connected to live Chromium-family browser via CDP')
|
|
1233
|
+
renderSlashOutput(`Endpoint: ${result.url || '(url unavailable)'}`)
|
|
1234
|
+
renderSlashOutput('next browser tool call will use this CDP endpoint')
|
|
1235
|
+
}
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Picker commands open a desktop overlay; a typed arg is resolved by that
|
|
1243
|
+
// picker so the command never dead-ends or falls through to the backend.
|
|
1244
|
+
const openPicker = async (pickerId: DesktopPickerId, ctx: SlashActionCtx): Promise<void> => {
|
|
1245
|
+
if (pickerId === 'model') {
|
|
1246
|
+
if (!ctx.arg.trim()) {
|
|
1247
|
+
setModelPickerOpen(true)
|
|
1248
|
+
|
|
1249
|
+
return
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Power users can still type `/model <name>` — run it on the backend.
|
|
1253
|
+
await runExec(ctx)
|
|
1254
|
+
|
|
1255
|
+
return
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// session picker — /resume, /sessions, /switch
|
|
1259
|
+
const query = ctx.arg.trim()
|
|
1260
|
+
|
|
1261
|
+
if (!query) {
|
|
1262
|
+
setSessionPickerOpen(true)
|
|
1263
|
+
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const sessions = $sessions.get()
|
|
1268
|
+
const lower = query.toLowerCase()
|
|
1269
|
+
|
|
1270
|
+
const match =
|
|
1271
|
+
sessions.find(session => session.id === query) ||
|
|
1272
|
+
sessions.find(session => sessionTitle(session).toLowerCase().includes(lower)) ||
|
|
1273
|
+
sessions.find(session => (session.preview ?? '').toLowerCase().includes(lower))
|
|
1274
|
+
|
|
1275
|
+
if (!match) {
|
|
1276
|
+
if (isSessionIdCandidate(query)) {
|
|
1277
|
+
await resumeStoredSession(query)
|
|
1278
|
+
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
notify({ kind: 'error', message: copy.resumeFailed })
|
|
1283
|
+
|
|
1284
|
+
return
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
await resumeStoredSession(match.id)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// The whole dispatcher: resolve the command's desktop surface, then act on
|
|
1291
|
+
// its kind. No per-command ladder — behavior lives in the registry.
|
|
1292
|
+
async function runSlash(commandText: string, sessionHint?: string, recordInput = true): Promise<void> {
|
|
1293
|
+
const command = commandText.trim()
|
|
1294
|
+
const { name, arg } = parseSlashCommand(command)
|
|
1295
|
+
|
|
1296
|
+
if (!name) {
|
|
1297
|
+
const sessionId = await ensureSessionId(sessionHint)
|
|
1298
|
+
|
|
1299
|
+
if (sessionId) {
|
|
1300
|
+
appendSessionTextMessage(sessionId, 'system', copy.emptySlashCommand)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
return
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const ctx: SlashActionCtx = { arg, command, name, recordInput, sessionHint }
|
|
1307
|
+
const surface = resolveDesktopCommand(`/${name}`)?.surface
|
|
1308
|
+
|
|
1309
|
+
switch (surface?.kind) {
|
|
1310
|
+
case 'unavailable': {
|
|
1311
|
+
const resolved = await withSlashOutput(ctx)
|
|
1312
|
+
resolved?.render(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
|
|
1313
|
+
|
|
1314
|
+
return
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
case 'picker':
|
|
1318
|
+
return openPicker(surface.picker, ctx)
|
|
1319
|
+
|
|
1320
|
+
case 'action':
|
|
1321
|
+
return actionHandlers[surface.action](ctx)
|
|
1322
|
+
|
|
1323
|
+
default:
|
|
1324
|
+
// exec spec, or an unknown skill / quick command the backend owns.
|
|
1325
|
+
return runExec(ctx)
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
515
1329
|
await runSlash(rawCommand, options?.sessionId, options?.recordInput ?? true)
|
|
516
1330
|
},
|
|
517
1331
|
[
|
|
@@ -519,9 +1333,13 @@ export function usePromptActions({
|
|
|
519
1333
|
appendSessionTextMessage,
|
|
520
1334
|
branchCurrentSession,
|
|
521
1335
|
busyRef,
|
|
1336
|
+
copy,
|
|
522
1337
|
createBackendSessionForSend,
|
|
523
1338
|
handleSkinCommand,
|
|
1339
|
+
handoffSession,
|
|
1340
|
+
refreshSessions,
|
|
524
1341
|
requestGateway,
|
|
1342
|
+
resumeStoredSession,
|
|
525
1343
|
startFreshSessionDraft,
|
|
526
1344
|
submitPromptText
|
|
527
1345
|
]
|
|
@@ -547,7 +1365,7 @@ export function usePromptActions({
|
|
|
547
1365
|
const transcribeVoiceAudio = useCallback(
|
|
548
1366
|
async (audio: Blob) => {
|
|
549
1367
|
if (!sttEnabled) {
|
|
550
|
-
throw new Error(
|
|
1368
|
+
throw new Error(copy.sttDisabled)
|
|
551
1369
|
}
|
|
552
1370
|
|
|
553
1371
|
const dataUrl = await blobToDataUrl(audio)
|
|
@@ -555,30 +1373,30 @@ export function usePromptActions({
|
|
|
555
1373
|
|
|
556
1374
|
return result.transcript
|
|
557
1375
|
},
|
|
558
|
-
[sttEnabled]
|
|
1376
|
+
[copy.sttDisabled, sttEnabled]
|
|
559
1377
|
)
|
|
560
1378
|
|
|
561
1379
|
const cancelRun = useCallback(async () => {
|
|
562
1380
|
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
1381
|
+
const releaseBusy = () => {
|
|
1382
|
+
setMutableRef(busyRef, false)
|
|
1383
|
+
setBusy(false)
|
|
1384
|
+
}
|
|
563
1385
|
|
|
564
|
-
busyRef.current = false
|
|
565
|
-
setBusy(false)
|
|
566
1386
|
setAwaitingResponse(false)
|
|
567
1387
|
|
|
568
|
-
const finalizeMessages = (messages: ChatMessage[]) =>
|
|
569
|
-
messages
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
: message
|
|
579
|
-
)
|
|
1388
|
+
const finalizeMessages = (messages: ChatMessage[], streamId?: string | null) =>
|
|
1389
|
+
messages
|
|
1390
|
+
.filter(
|
|
1391
|
+
message =>
|
|
1392
|
+
!((message.pending || message.id === streamId) && !chatMessageText(message).trim())
|
|
1393
|
+
)
|
|
1394
|
+
.map(message =>
|
|
1395
|
+
message.pending || message.id === streamId ? { ...message, pending: false } : message
|
|
1396
|
+
)
|
|
580
1397
|
|
|
581
1398
|
if (!sessionId) {
|
|
1399
|
+
releaseBusy()
|
|
582
1400
|
setMessages(finalizeMessages($messages.get()))
|
|
583
1401
|
|
|
584
1402
|
return
|
|
@@ -586,20 +1404,7 @@ export function usePromptActions({
|
|
|
586
1404
|
|
|
587
1405
|
updateSessionState(sessionId, state => {
|
|
588
1406
|
const streamId = state.streamId
|
|
589
|
-
|
|
590
|
-
const messages = streamId
|
|
591
|
-
? state.messages.map(message =>
|
|
592
|
-
message.id === streamId
|
|
593
|
-
? {
|
|
594
|
-
...message,
|
|
595
|
-
parts: chatMessageText(message).trim()
|
|
596
|
-
? appendTextPart(message.parts, INTERRUPTED_MARKER)
|
|
597
|
-
: [...message.parts, textPart(INTERRUPTED_MARKER.trim())],
|
|
598
|
-
pending: false
|
|
599
|
-
}
|
|
600
|
-
: message
|
|
601
|
-
)
|
|
602
|
-
: finalizeMessages(state.messages)
|
|
1407
|
+
const messages = finalizeMessages(state.messages, streamId)
|
|
603
1408
|
|
|
604
1409
|
return {
|
|
605
1410
|
...state,
|
|
@@ -612,12 +1417,82 @@ export function usePromptActions({
|
|
|
612
1417
|
}
|
|
613
1418
|
})
|
|
614
1419
|
|
|
1420
|
+
clearSessionTodos(sessionId)
|
|
1421
|
+
clearSessionSubagents(sessionId)
|
|
1422
|
+
resetSessionBackground(sessionId)
|
|
1423
|
+
|
|
615
1424
|
try {
|
|
616
1425
|
await requestGateway('session.interrupt', { session_id: sessionId })
|
|
1426
|
+
releaseBusy()
|
|
617
1427
|
} catch (err) {
|
|
618
|
-
|
|
1428
|
+
let stopError = err
|
|
1429
|
+
|
|
1430
|
+
if (isSessionNotFoundError(err) && selectedStoredSessionIdRef.current) {
|
|
1431
|
+
try {
|
|
1432
|
+
const resumed = await requestGateway<{ session_id: string }>('session.resume', {
|
|
1433
|
+
session_id: selectedStoredSessionIdRef.current
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
const recoveredId = resumed?.session_id
|
|
1437
|
+
|
|
1438
|
+
if (recoveredId) {
|
|
1439
|
+
activeSessionIdRef.current = recoveredId
|
|
1440
|
+
await requestGateway('session.interrupt', { session_id: recoveredId })
|
|
1441
|
+
releaseBusy()
|
|
1442
|
+
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
} catch (resumeErr) {
|
|
1446
|
+
stopError = resumeErr
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
releaseBusy()
|
|
1451
|
+
notifyError(stopError, copy.stopFailed)
|
|
619
1452
|
}
|
|
620
|
-
}, [
|
|
1453
|
+
}, [
|
|
1454
|
+
activeSessionId,
|
|
1455
|
+
activeSessionIdRef,
|
|
1456
|
+
busyRef,
|
|
1457
|
+
copy.stopFailed,
|
|
1458
|
+
requestGateway,
|
|
1459
|
+
selectedStoredSessionIdRef,
|
|
1460
|
+
updateSessionState
|
|
1461
|
+
])
|
|
1462
|
+
|
|
1463
|
+
// Steer = nudge the live turn without interrupting: the gateway appends the
|
|
1464
|
+
// text to the next tool result so the model reads it on its next iteration
|
|
1465
|
+
// (desktop parity with `/steer`). Returns false on reject (no live tool
|
|
1466
|
+
// window) so the caller can fall back to queueing the words for the next turn.
|
|
1467
|
+
const steerPrompt = useCallback(
|
|
1468
|
+
async (rawText: string): Promise<boolean> => {
|
|
1469
|
+
const text = rawText.trim()
|
|
1470
|
+
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
1471
|
+
|
|
1472
|
+
if (!text || !sessionId) {
|
|
1473
|
+
return false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
try {
|
|
1477
|
+
const result = await requestGateway<SessionSteerResponse>('session.steer', { session_id: sessionId, text })
|
|
1478
|
+
|
|
1479
|
+
if (result?.status === 'queued') {
|
|
1480
|
+
triggerHaptic('submit')
|
|
1481
|
+
// Inline note (not a toast) so the nudge lives in the transcript next
|
|
1482
|
+
// to the turn it steered. The `steer:` prefix is rendered as a codicon
|
|
1483
|
+
// row by SystemMessage (see STEER_NOTE_RE), same style as slash output.
|
|
1484
|
+
appendSessionTextMessage(sessionId, 'system', `steer:${text}`)
|
|
1485
|
+
|
|
1486
|
+
return true
|
|
1487
|
+
}
|
|
1488
|
+
} catch {
|
|
1489
|
+
// Swallow — caller queues the text so nothing is lost.
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return false
|
|
1493
|
+
},
|
|
1494
|
+
[activeSessionId, activeSessionIdRef, appendSessionTextMessage, requestGateway]
|
|
1495
|
+
)
|
|
621
1496
|
|
|
622
1497
|
const reloadFromMessage = useCallback(
|
|
623
1498
|
async (parentId: string | null) => {
|
|
@@ -689,10 +1564,98 @@ export function usePromptActions({
|
|
|
689
1564
|
busy: false,
|
|
690
1565
|
awaitingResponse: false
|
|
691
1566
|
}))
|
|
692
|
-
notifyError(err,
|
|
1567
|
+
notifyError(err, copy.regenerateFailed)
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
[activeSessionId, copy.regenerateFailed, requestGateway, updateSessionState]
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
// Cursor-style "restore checkpoint": rewind the conversation to a past user
|
|
1574
|
+
// prompt and run it again from there. Reuses the edit composer's rewind
|
|
1575
|
+
// mechanism — `prompt.submit` with `truncate_before_user_ordinal` drops that
|
|
1576
|
+
// user turn and everything after it from the session history, then the same
|
|
1577
|
+
// text is submitted as a fresh turn. Callers confirm before invoking; errors
|
|
1578
|
+
// are rethrown so the confirmation dialog can surface them inline.
|
|
1579
|
+
// Submit a rewind (truncate-before-ordinal + resubmit). Because edit/restore
|
|
1580
|
+
// can fire while a turn is streaming, interrupt the live turn first — the
|
|
1581
|
+
// cooperative interrupt takes a beat, so the shared busy-retry rides it out.
|
|
1582
|
+
const submitRewindPrompt = useCallback(
|
|
1583
|
+
async (sessionId: string, text: string, truncateOrdinal: number | undefined, wasRunning: boolean) => {
|
|
1584
|
+
if (wasRunning) {
|
|
1585
|
+
try {
|
|
1586
|
+
await requestGateway('session.interrupt', { session_id: sessionId })
|
|
1587
|
+
} catch {
|
|
1588
|
+
// Best-effort — the busy-retry below still gates the submit.
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
await withSessionBusyRetry(() =>
|
|
1593
|
+
requestGateway('prompt.submit', {
|
|
1594
|
+
session_id: sessionId,
|
|
1595
|
+
text,
|
|
1596
|
+
...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
|
|
1597
|
+
})
|
|
1598
|
+
)
|
|
1599
|
+
},
|
|
1600
|
+
[requestGateway]
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
const restoreToMessage = useCallback(
|
|
1604
|
+
async (messageId: string) => {
|
|
1605
|
+
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
1606
|
+
|
|
1607
|
+
if (!sessionId) {
|
|
1608
|
+
return
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const messages = $messages.get()
|
|
1612
|
+
const sourceIndex = messages.findIndex(m => m.id === messageId)
|
|
1613
|
+
const source = messages[sourceIndex]
|
|
1614
|
+
|
|
1615
|
+
if (!source || source.role !== 'user') {
|
|
1616
|
+
return
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const text = chatMessageText(source).trim()
|
|
1620
|
+
|
|
1621
|
+
if (!text) {
|
|
1622
|
+
return
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const wasRunning = $busy.get()
|
|
1626
|
+
const truncateBeforeUserOrdinal = visibleUserOrdinal(messages, sourceIndex)
|
|
1627
|
+
|
|
1628
|
+
// The turns we're discarding may have spawned todos and background
|
|
1629
|
+
// processes; they belong to the abandoned timeline, so wipe their status
|
|
1630
|
+
// rows (and kill the live processes) before the fresh run repopulates.
|
|
1631
|
+
clearSessionTodos(sessionId)
|
|
1632
|
+
resetSessionBackground(sessionId)
|
|
1633
|
+
|
|
1634
|
+
clearNotifications()
|
|
1635
|
+
setMutableRef(busyRef, true)
|
|
1636
|
+
setBusy(true)
|
|
1637
|
+
setAwaitingResponse(true)
|
|
1638
|
+
updateSessionState(sessionId, state => ({
|
|
1639
|
+
...state,
|
|
1640
|
+
busy: true,
|
|
1641
|
+
awaitingResponse: true,
|
|
1642
|
+
pendingBranchGroup: null,
|
|
1643
|
+
sawAssistantPayload: false,
|
|
1644
|
+
interrupted: false,
|
|
1645
|
+
messages: state.messages.slice(0, sourceIndex + 1)
|
|
1646
|
+
}))
|
|
1647
|
+
|
|
1648
|
+
try {
|
|
1649
|
+
await submitRewindPrompt(sessionId, text, truncateBeforeUserOrdinal, wasRunning)
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
setMutableRef(busyRef, false)
|
|
1652
|
+
setBusy(false)
|
|
1653
|
+
setAwaitingResponse(false)
|
|
1654
|
+
updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
|
|
1655
|
+
throw err
|
|
693
1656
|
}
|
|
694
1657
|
},
|
|
695
|
-
[activeSessionId,
|
|
1658
|
+
[activeSessionId, activeSessionIdRef, busyRef, submitRewindPrompt, updateSessionState]
|
|
696
1659
|
)
|
|
697
1660
|
|
|
698
1661
|
const editMessage = useCallback(
|
|
@@ -701,7 +1664,7 @@ export function usePromptActions({
|
|
|
701
1664
|
const sourceId = edited.sourceId || edited.parentId
|
|
702
1665
|
const text = appendText(edited)
|
|
703
1666
|
|
|
704
|
-
if (!sessionId || !sourceId || !text || edited.role !== 'user'
|
|
1667
|
+
if (!sessionId || !sourceId || !text || edited.role !== 'user') {
|
|
705
1668
|
return
|
|
706
1669
|
}
|
|
707
1670
|
|
|
@@ -713,14 +1676,25 @@ export function usePromptActions({
|
|
|
713
1676
|
return
|
|
714
1677
|
}
|
|
715
1678
|
|
|
1679
|
+
// Sending an edit is a revert: rewind to this prompt and re-run with the
|
|
1680
|
+
// new text. It can fire mid-turn, so capture the live state — the submit
|
|
1681
|
+
// helper interrupts first when a turn is running.
|
|
1682
|
+
const wasRunning = $busy.get()
|
|
1683
|
+
|
|
716
1684
|
// Failed turn: optimistic user msg never reached the gateway, so truncating
|
|
717
1685
|
// by ordinal would 422. Submit as a plain resend instead.
|
|
718
1686
|
const nextMessage = messages[sourceIndex + 1]
|
|
719
1687
|
const isFailedTurn = nextMessage?.role === 'assistant' && Boolean(nextMessage.error)
|
|
720
1688
|
const editedMessage: ChatMessage = { ...source, parts: [textPart(text)] }
|
|
721
1689
|
|
|
1690
|
+
// Editing rewinds the conversation to this prompt — same as restore — so
|
|
1691
|
+
// drop the abandoned timeline's todos/background rows (and kill the live
|
|
1692
|
+
// processes) before the re-run repopulates them.
|
|
1693
|
+
clearSessionTodos(sessionId)
|
|
1694
|
+
resetSessionBackground(sessionId)
|
|
1695
|
+
|
|
722
1696
|
clearNotifications()
|
|
723
|
-
busyRef
|
|
1697
|
+
setMutableRef(busyRef, true)
|
|
724
1698
|
setBusy(true)
|
|
725
1699
|
setAwaitingResponse(true)
|
|
726
1700
|
updateSessionState(sessionId, state => ({
|
|
@@ -733,24 +1707,18 @@ export function usePromptActions({
|
|
|
733
1707
|
messages: [...state.messages.slice(0, sourceIndex), editedMessage]
|
|
734
1708
|
}))
|
|
735
1709
|
|
|
736
|
-
const submit = (truncateOrdinal?: number) =>
|
|
737
|
-
requestGateway('prompt.submit', {
|
|
738
|
-
session_id: sessionId,
|
|
739
|
-
text,
|
|
740
|
-
...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
|
|
741
|
-
})
|
|
742
|
-
|
|
743
1710
|
const isStaleTargetError = (err: unknown) =>
|
|
744
1711
|
/no longer in session history|not in session history/i.test(err instanceof Error ? err.message : String(err))
|
|
745
1712
|
|
|
746
1713
|
try {
|
|
747
|
-
await
|
|
1714
|
+
await submitRewindPrompt(sessionId, text, isFailedTurn ? undefined : visibleUserOrdinal(messages, sourceIndex), wasRunning)
|
|
748
1715
|
} catch (err) {
|
|
749
1716
|
let surfaced = err
|
|
750
1717
|
|
|
751
1718
|
if (!isFailedTurn && isStaleTargetError(err)) {
|
|
752
1719
|
try {
|
|
753
|
-
|
|
1720
|
+
// Already interrupted on the first attempt — submit as a plain resend.
|
|
1721
|
+
await submitRewindPrompt(sessionId, text, undefined, false)
|
|
754
1722
|
|
|
755
1723
|
return
|
|
756
1724
|
} catch (retryErr) {
|
|
@@ -758,14 +1726,14 @@ export function usePromptActions({
|
|
|
758
1726
|
}
|
|
759
1727
|
}
|
|
760
1728
|
|
|
761
|
-
busyRef
|
|
1729
|
+
setMutableRef(busyRef, false)
|
|
762
1730
|
setBusy(false)
|
|
763
1731
|
setAwaitingResponse(false)
|
|
764
1732
|
updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
|
|
765
|
-
notifyError(surfaced,
|
|
1733
|
+
notifyError(surfaced, copy.editFailed)
|
|
766
1734
|
}
|
|
767
1735
|
},
|
|
768
|
-
[activeSessionId, activeSessionIdRef, busyRef,
|
|
1736
|
+
[activeSessionId, activeSessionIdRef, busyRef, copy.editFailed, submitRewindPrompt, updateSessionState]
|
|
769
1737
|
)
|
|
770
1738
|
|
|
771
1739
|
const handleThreadMessagesChange = useCallback(
|
|
@@ -802,5 +1770,15 @@ export function usePromptActions({
|
|
|
802
1770
|
[activeSessionIdRef, updateSessionState]
|
|
803
1771
|
)
|
|
804
1772
|
|
|
805
|
-
return {
|
|
1773
|
+
return {
|
|
1774
|
+
cancelRun,
|
|
1775
|
+
editMessage,
|
|
1776
|
+
handleThreadMessagesChange,
|
|
1777
|
+
handoffSession,
|
|
1778
|
+
reloadFromMessage,
|
|
1779
|
+
restoreToMessage,
|
|
1780
|
+
steerPrompt,
|
|
1781
|
+
submitText,
|
|
1782
|
+
transcribeVoiceAudio
|
|
1783
|
+
}
|
|
806
1784
|
}
|