@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
|
@@ -9,6 +9,7 @@ Uses python-telegram-bot library for:
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import dataclasses
|
|
12
|
+
import inspect
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
14
15
|
import os
|
|
@@ -181,6 +182,8 @@ def _strip_mdv2(text: str) -> str:
|
|
|
181
182
|
"""
|
|
182
183
|
# Remove escape backslashes before special characters
|
|
183
184
|
cleaned = re.sub(r'\\([_*\[\]()~`>#\+\-=|{}.!\\])', r'\1', text)
|
|
185
|
+
# Remove standard markdown bold (**text** → text) BEFORE MarkdownV2 bold
|
|
186
|
+
cleaned = re.sub(r'\*\*([^*]+)\*\*', r'\1', cleaned)
|
|
184
187
|
# Remove MarkdownV2 bold markers that format_message converted from **bold**
|
|
185
188
|
cleaned = re.sub(r'\*([^*]+)\*', r'\1', cleaned)
|
|
186
189
|
# Remove MarkdownV2 italic markers that format_message converted from *italic*
|
|
@@ -344,6 +347,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
344
347
|
|
|
345
348
|
# Telegram message limits
|
|
346
349
|
MAX_MESSAGE_LENGTH = 4096
|
|
350
|
+
supports_code_blocks = True # Telegram MarkdownV2 renders fenced code blocks
|
|
351
|
+
# Bot API 10.1 Rich Messages cap the raw markdown/html text at 32,768
|
|
352
|
+
# UTF-8 characters. Content above this is sent via the legacy chunking path.
|
|
353
|
+
RICH_MESSAGE_MAX_CHARS = 32768
|
|
354
|
+
# Backwards-compatible alias for tests/external callers that referenced the
|
|
355
|
+
# initial implementation name. The API limit is character-based, not bytes.
|
|
356
|
+
RICH_MESSAGE_MAX_BYTES = RICH_MESSAGE_MAX_CHARS
|
|
347
357
|
# Threshold for detecting Telegram client-side message splits.
|
|
348
358
|
# When a chunk is near this limit, a continuation is almost certain.
|
|
349
359
|
_SPLIT_THRESHOLD = 4000
|
|
@@ -409,6 +419,18 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
409
419
|
self._mention_patterns = self._compile_mention_patterns()
|
|
410
420
|
self._reply_to_mode: str = getattr(config, 'reply_to_mode', 'first') or 'first'
|
|
411
421
|
self._disable_link_previews: bool = self._coerce_bool_extra("disable_link_previews", False)
|
|
422
|
+
# Bot API 10.1 Rich Messages: render constructs the legacy MarkdownV2
|
|
423
|
+
# path degrades (tables → bullet lists, task lists, <details>, block
|
|
424
|
+
# math) via sendRichMessage / editMessageText's rich_message param using
|
|
425
|
+
# the raw agent markdown. Enabled by default; users can opt out for
|
|
426
|
+
# clients that accept but render rich messages poorly via
|
|
427
|
+
# platforms.telegram.extra.rich_messages: false.
|
|
428
|
+
self._rich_messages_enabled: bool = self._coerce_bool_extra("rich_messages", True)
|
|
429
|
+
# Latched off after a capability failure on sendRichMessage /
|
|
430
|
+
# sendRichMessageDraft (e.g. older python-telegram-bot without the
|
|
431
|
+
# endpoint) so later sends skip the doomed rich attempt entirely.
|
|
432
|
+
self._rich_send_disabled: bool = False
|
|
433
|
+
self._rich_draft_disabled: bool = False
|
|
412
434
|
# Buffer rapid/album photo updates so Telegram image bursts are handled
|
|
413
435
|
# as a single MessageEvent instead of self-interrupting multiple turns.
|
|
414
436
|
self._media_batch_delay_seconds = float(os.getenv("HERMES_TELEGRAM_MEDIA_BATCH_DELAY_SECONDS", "0.8"))
|
|
@@ -454,6 +476,23 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
454
476
|
self._forum_command_registered: set[int] = set()
|
|
455
477
|
# Lock per la registrazione sicura dei comandi nei forum supergroup
|
|
456
478
|
self._forum_lock = asyncio.Lock()
|
|
479
|
+
# Status indicator: when enabled, the bot's short description (the line
|
|
480
|
+
# shown under its name in the profile) is set to "Online" on connect and
|
|
481
|
+
# "Offline" on clean disconnect, so users can tell whether the gateway is
|
|
482
|
+
# up. Telegram bots have no real presence/online dot (that's a user-account
|
|
483
|
+
# feature), so the short description is the closest available surface.
|
|
484
|
+
# Off by default — this mutates the bot's GLOBAL profile, visible to all
|
|
485
|
+
# users. Opt in via gateway config: extra.status_indicator: true, or set
|
|
486
|
+
# custom strings via extra.status_online / extra.status_offline.
|
|
487
|
+
self._status_indicator_enabled: bool = bool(
|
|
488
|
+
self.config.extra.get("status_indicator", False)
|
|
489
|
+
)
|
|
490
|
+
self._status_online_text: str = str(
|
|
491
|
+
self.config.extra.get("status_online", "Online")
|
|
492
|
+
)
|
|
493
|
+
self._status_offline_text: str = str(
|
|
494
|
+
self.config.extra.get("status_offline", "Offline")
|
|
495
|
+
)
|
|
457
496
|
# DM Topics config from extra.dm_topics
|
|
458
497
|
self._dm_topics_config: List[Dict[str, Any]] = self.config.extra.get("dm_topics", [])
|
|
459
498
|
# Precomputed chat_ids that have DM topics configured (for O(1) root-DM ignore check)
|
|
@@ -899,6 +938,456 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
899
938
|
return {"link_preview_options": LinkPreviewOptions(is_disabled=True)}
|
|
900
939
|
return {"disable_web_page_preview": True}
|
|
901
940
|
|
|
941
|
+
# ------------------------------------------------------------------
|
|
942
|
+
# Bot API 10.1 Rich Messages (sendRichMessage)
|
|
943
|
+
#
|
|
944
|
+
# Final / new-message replies opportunistically use sendRichMessage with
|
|
945
|
+
# the RAW agent markdown so richer constructs (tables, task lists,
|
|
946
|
+
# collapsible details, math, ...) render natively. The legacy MarkdownV2
|
|
947
|
+
# send() path stays as the fallback for unsupported/oversized content and
|
|
948
|
+
# older PTB/clients. Streaming edits stay on Hermes' existing MarkdownV2
|
|
949
|
+
# edit path for now; finalization can re-send as rich and delete the stale
|
|
950
|
+
# preview until rich_message edit support is wired directly.
|
|
951
|
+
# ------------------------------------------------------------------
|
|
952
|
+
def _content_fits_rich_limits(self, content: str) -> bool:
|
|
953
|
+
"""Cheap pre-check for the one hard rich limit we can count locally.
|
|
954
|
+
|
|
955
|
+
Only the 32,768 UTF-8 character text cap is enforced here. Other Bot API
|
|
956
|
+
rich limits (500 blocks, 16 nesting levels, 20 table columns, ...) are
|
|
957
|
+
not pre-counted; if exceeded Telegram returns a BadRequest, which
|
|
958
|
+
:meth:`_is_rich_fallback_error` classifies as permanent so the send
|
|
959
|
+
degrades to the legacy chunking path.
|
|
960
|
+
"""
|
|
961
|
+
return len(content) <= self.RICH_MESSAGE_MAX_CHARS
|
|
962
|
+
|
|
963
|
+
def _bot_supports_rich(self) -> bool:
|
|
964
|
+
"""True when the bound bot can issue raw ``sendRichMessage`` calls.
|
|
965
|
+
|
|
966
|
+
Gates on ``do_api_request`` being an *async* callable. The real
|
|
967
|
+
``telegram.Bot.do_api_request`` is a coroutine function; test doubles
|
|
968
|
+
that opt into rich set it to an ``AsyncMock`` (also a coroutine
|
|
969
|
+
function). Plain ``MagicMock`` bots expose a *sync* auto-child and
|
|
970
|
+
``SimpleNamespace`` bots lack the attribute entirely — both resolve to
|
|
971
|
+
``False`` here, so the legacy path is used unchanged.
|
|
972
|
+
"""
|
|
973
|
+
return inspect.iscoroutinefunction(getattr(self._bot, "do_api_request", None))
|
|
974
|
+
|
|
975
|
+
_RICH_DETAILS_RE = re.compile(r"<details\b[^>]*>.*?</details>", re.IGNORECASE | re.DOTALL)
|
|
976
|
+
_RICH_MATH_IN_DETAILS_RE = re.compile(
|
|
977
|
+
r"(\$\$.*?\$\$|"
|
|
978
|
+
r"\\\[.*?\\\]|"
|
|
979
|
+
r"\\\(.*?\\\)|"
|
|
980
|
+
r"\\(?:sum|frac|alpha|beta|gamma|delta|theta|lambda|mu|pi|sigma|"
|
|
981
|
+
r"int|prod|sqrt|lim|infty|begin\{(?:equation|align|matrix|cases)\}))",
|
|
982
|
+
re.IGNORECASE | re.DOTALL,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool:
|
|
986
|
+
"""Return True for rich-message details+math content that crashes TDesktop.
|
|
987
|
+
|
|
988
|
+
Telegram Desktop 6.9.1 can crash while rendering Bot API 10.1 rich
|
|
989
|
+
messages containing math inside a collapsible details block
|
|
990
|
+
(telegramdesktop/tdesktop#30808). The Bot API accepts the payload, so
|
|
991
|
+
Hermes must skip rich delivery up front and use the legacy MarkdownV2
|
|
992
|
+
path until affected Desktop clients age out.
|
|
993
|
+
"""
|
|
994
|
+
if not content:
|
|
995
|
+
return False
|
|
996
|
+
for details_block in self._RICH_DETAILS_RE.findall(content):
|
|
997
|
+
if self._RICH_MATH_IN_DETAILS_RE.search(details_block):
|
|
998
|
+
return True
|
|
999
|
+
return False
|
|
1000
|
+
|
|
1001
|
+
def _needs_rich_rendering(self, content: str) -> bool:
|
|
1002
|
+
"""Return True for markdown constructs that the legacy path degrades.
|
|
1003
|
+
|
|
1004
|
+
Keep ordinary replies on the pre-rich MarkdownV2 path so Telegram
|
|
1005
|
+
clients render a consistent font weight/spacing. The rich endpoint is
|
|
1006
|
+
reserved for constructs where raw markdown materially improves output:
|
|
1007
|
+
pipe tables (MarkdownV2 has no table syntax and rewrites them into
|
|
1008
|
+
bullet lists), GFM task lists, collapsible ``<details>`` blocks, and
|
|
1009
|
+
block math. Adapted from #45995 (@YonganZhang).
|
|
1010
|
+
"""
|
|
1011
|
+
if not content:
|
|
1012
|
+
return False
|
|
1013
|
+
if any(_TABLE_SEPARATOR_RE.match(line) for line in content.splitlines()):
|
|
1014
|
+
return True
|
|
1015
|
+
if re.search(r"(?m)^\s*[-*]\s+\[[ xX]\]\s+", content):
|
|
1016
|
+
return True
|
|
1017
|
+
if re.search(r"(?m)^<details\b|^</details>|^<summary\b|^</summary>", content):
|
|
1018
|
+
return True
|
|
1019
|
+
if "$$" in content:
|
|
1020
|
+
return True
|
|
1021
|
+
return False
|
|
1022
|
+
|
|
1023
|
+
def _rich_eligible(self, content: str) -> bool:
|
|
1024
|
+
"""Capability/content eligibility for rich, ignoring ``expect_edits``.
|
|
1025
|
+
|
|
1026
|
+
Shared core of :meth:`_should_attempt_rich` minus the per-call
|
|
1027
|
+
``expect_edits`` metadata gate. The rich EDIT-finalize path
|
|
1028
|
+
(:meth:`_try_edit_rich`) needs this: a streamed preview is sent with
|
|
1029
|
+
``expect_edits=True`` to stay on the editable path mid-stream, but the
|
|
1030
|
+
FINAL edit should still upgrade to rich when the content warrants it.
|
|
1031
|
+
"""
|
|
1032
|
+
return bool(
|
|
1033
|
+
getattr(self, "_rich_messages_enabled", True)
|
|
1034
|
+
and not getattr(self, "_rich_send_disabled", False)
|
|
1035
|
+
and content
|
|
1036
|
+
and content.strip()
|
|
1037
|
+
and self._needs_rich_rendering(content)
|
|
1038
|
+
and not self._has_telegram_desktop_details_math_crash_shape(content)
|
|
1039
|
+
and self._content_fits_rich_limits(content)
|
|
1040
|
+
and self._bot_supports_rich()
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
def _should_attempt_rich(
|
|
1044
|
+
self, content: str, metadata: Optional[Dict[str, Any]] = None
|
|
1045
|
+
) -> bool:
|
|
1046
|
+
return bool(
|
|
1047
|
+
not (metadata or {}).get("expect_edits")
|
|
1048
|
+
and self._rich_eligible(content)
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
def prefers_fresh_final_streaming(
|
|
1052
|
+
self, content: str, metadata: Optional[Dict[str, Any]] = None
|
|
1053
|
+
) -> bool:
|
|
1054
|
+
"""Whether to replace a streamed preview with a fresh rich final.
|
|
1055
|
+
|
|
1056
|
+
Disabled for Telegram. The fresh-final path briefly shows two copies of
|
|
1057
|
+
the final answer, then deletes the streaming preview after the rich send
|
|
1058
|
+
succeeds — it looks like duplicate delivery at the end of every streamed
|
|
1059
|
+
turn (the reason #46206 reverted it). Rich finalize is instead handled
|
|
1060
|
+
by editing the existing preview in place via Bot API 10.1's
|
|
1061
|
+
``editMessageText`` ``rich_message`` parameter (see
|
|
1062
|
+
:meth:`_try_edit_rich`), so no fresh re-send / delete is needed.
|
|
1063
|
+
"""
|
|
1064
|
+
return False
|
|
1065
|
+
|
|
1066
|
+
def streaming_overflow_limit(self) -> Optional[int]:
|
|
1067
|
+
"""Allow the stream consumer to accumulate up to the rich-message cap
|
|
1068
|
+
before splitting, so a reply that fits one ``sendRichMessage`` /
|
|
1069
|
+
``sendRichMessageDraft`` isn't fragmented at the 4,096 MarkdownV2 limit.
|
|
1070
|
+
|
|
1071
|
+
Gated on the same rich capability as the send path (minus the
|
|
1072
|
+
content-length check — raising that cap is the whole point): rich not
|
|
1073
|
+
latched off and the bot exposes an async ``do_api_request``. Returns
|
|
1074
|
+
``None`` (→ legacy 4,096 limit) when rich isn't available, so non-rich
|
|
1075
|
+
streams split exactly as before.
|
|
1076
|
+
"""
|
|
1077
|
+
if (
|
|
1078
|
+
getattr(self, "_rich_messages_enabled", True)
|
|
1079
|
+
and not getattr(self, "_rich_send_disabled", False)
|
|
1080
|
+
and self._bot_supports_rich()
|
|
1081
|
+
):
|
|
1082
|
+
return self.RICH_MESSAGE_MAX_CHARS
|
|
1083
|
+
return None
|
|
1084
|
+
|
|
1085
|
+
def _rich_message_payload(
|
|
1086
|
+
self, content: str, *, skip_entity_detection: bool = False
|
|
1087
|
+
) -> Dict[str, Any]:
|
|
1088
|
+
"""Build the ``InputRichMessage`` object from RAW markdown.
|
|
1089
|
+
|
|
1090
|
+
Never pass ``format_message(content)`` here — that converts to
|
|
1091
|
+
MarkdownV2 and would escape/destroy rich syntax like table pipes.
|
|
1092
|
+
"""
|
|
1093
|
+
payload: Dict[str, Any] = {"markdown": content}
|
|
1094
|
+
if skip_entity_detection:
|
|
1095
|
+
payload["skip_entity_detection"] = True
|
|
1096
|
+
return payload
|
|
1097
|
+
|
|
1098
|
+
def _is_rich_capability_error(self, exc: Exception) -> bool:
|
|
1099
|
+
"""True ⇒ the rich endpoint itself is unavailable (old PTB/server).
|
|
1100
|
+
|
|
1101
|
+
These latch rich off for the rest of the adapter's life — retrying is
|
|
1102
|
+
pointless and would cost a failed roundtrip on every send. Per-message
|
|
1103
|
+
rejections (BadRequest from a parser/limit issue) are NOT capability
|
|
1104
|
+
errors: the next message may be fine.
|
|
1105
|
+
"""
|
|
1106
|
+
name = exc.__class__.__name__.lower()
|
|
1107
|
+
if name in {"endpointnotfound", "invalidtoken"}:
|
|
1108
|
+
return True
|
|
1109
|
+
if isinstance(exc, (AttributeError, TypeError, NotImplementedError)):
|
|
1110
|
+
return True
|
|
1111
|
+
if getattr(exc, "error_code", None) == 404:
|
|
1112
|
+
return True
|
|
1113
|
+
s = str(exc).lower()
|
|
1114
|
+
if ("method" in s or "endpoint" in s) and (
|
|
1115
|
+
"not found" in s or "does not exist" in s
|
|
1116
|
+
):
|
|
1117
|
+
return True
|
|
1118
|
+
return "no such method" in s
|
|
1119
|
+
|
|
1120
|
+
def _is_rich_fallback_error(self, exc: Exception) -> bool:
|
|
1121
|
+
"""True ⇒ permanent/capability error ⇒ safe to fall back to legacy.
|
|
1122
|
+
|
|
1123
|
+
Conservative on purpose: only clearly-permanent failures (BadRequest,
|
|
1124
|
+
capability errors, unknown/unsupported endpoint) qualify. Everything
|
|
1125
|
+
else is treated as transient — the rich request may have reached
|
|
1126
|
+
Telegram, so we must NOT legacy-resend and risk a duplicate.
|
|
1127
|
+
"""
|
|
1128
|
+
if self._is_bad_request_error(exc):
|
|
1129
|
+
return True
|
|
1130
|
+
if self._is_rich_capability_error(exc):
|
|
1131
|
+
return True
|
|
1132
|
+
s = str(exc).lower()
|
|
1133
|
+
return "unsupported" in s or "not implemented" in s
|
|
1134
|
+
|
|
1135
|
+
def _compute_single_send_routing(
|
|
1136
|
+
self,
|
|
1137
|
+
chat_id: str,
|
|
1138
|
+
reply_to: Optional[str],
|
|
1139
|
+
metadata: Optional[Dict[str, Any]],
|
|
1140
|
+
thread_id: Optional[str],
|
|
1141
|
+
) -> Optional[tuple]:
|
|
1142
|
+
"""Routing for a single (rich) send — mirrors send()'s index-0 block.
|
|
1143
|
+
|
|
1144
|
+
Returns ``(reply_to_id, thread_kwargs)``, or ``None`` to signal "skip
|
|
1145
|
+
rich, let the legacy path handle it" — used for the DM-topic fail-loud
|
|
1146
|
+
case so the legacy path stays the single source of the refuse result.
|
|
1147
|
+
"""
|
|
1148
|
+
metadata_reply_to = self._metadata_reply_to_message_id(metadata)
|
|
1149
|
+
private_dm_topic_send = self._is_private_dm_topic_send(chat_id, thread_id, metadata)
|
|
1150
|
+
dm_topic_reply_to_off = (
|
|
1151
|
+
private_dm_topic_send
|
|
1152
|
+
and self._reply_to_mode == "off"
|
|
1153
|
+
and bool(metadata and metadata.get("telegram_dm_topic_reply_fallback"))
|
|
1154
|
+
)
|
|
1155
|
+
reply_to_source = reply_to or (
|
|
1156
|
+
str(metadata_reply_to)
|
|
1157
|
+
if private_dm_topic_send and metadata_reply_to is not None
|
|
1158
|
+
else None
|
|
1159
|
+
)
|
|
1160
|
+
if private_dm_topic_send:
|
|
1161
|
+
should_thread = reply_to_source is not None and self._reply_to_mode != "off"
|
|
1162
|
+
else:
|
|
1163
|
+
should_thread = self._should_thread_reply(reply_to_source, 0)
|
|
1164
|
+
reply_to_id = int(reply_to_source) if should_thread and reply_to_source else None
|
|
1165
|
+
if private_dm_topic_send and reply_to_id is None and not dm_topic_reply_to_off:
|
|
1166
|
+
# Refusing to send outside the requested DM topic — defer to the
|
|
1167
|
+
# legacy path, which returns the canonical fail-loud SendResult.
|
|
1168
|
+
return None
|
|
1169
|
+
thread_kwargs = self._thread_kwargs_for_send(
|
|
1170
|
+
chat_id,
|
|
1171
|
+
thread_id,
|
|
1172
|
+
metadata,
|
|
1173
|
+
reply_to_message_id=reply_to_id,
|
|
1174
|
+
reply_to_mode=self._reply_to_mode,
|
|
1175
|
+
)
|
|
1176
|
+
return reply_to_id, thread_kwargs
|
|
1177
|
+
|
|
1178
|
+
async def _try_send_rich(
|
|
1179
|
+
self,
|
|
1180
|
+
chat_id: str,
|
|
1181
|
+
content: str,
|
|
1182
|
+
reply_to: Optional[str],
|
|
1183
|
+
metadata: Optional[Dict[str, Any]],
|
|
1184
|
+
) -> Optional[SendResult]:
|
|
1185
|
+
"""Attempt a single ``sendRichMessage`` send.
|
|
1186
|
+
|
|
1187
|
+
Returns a :class:`SendResult` (success, or a transient failure that the
|
|
1188
|
+
caller must NOT legacy-resend), or ``None`` to signal "fall back to the
|
|
1189
|
+
legacy MarkdownV2 path" (permanent/capability error or DM-topic skip).
|
|
1190
|
+
"""
|
|
1191
|
+
thread_id = self._metadata_thread_id(metadata)
|
|
1192
|
+
routing = self._compute_single_send_routing(chat_id, reply_to, metadata, thread_id)
|
|
1193
|
+
if routing is None:
|
|
1194
|
+
return None
|
|
1195
|
+
reply_to_id, thread_kwargs = routing
|
|
1196
|
+
|
|
1197
|
+
payload: Dict[str, Any] = {
|
|
1198
|
+
"chat_id": int(chat_id),
|
|
1199
|
+
"rich_message": self._rich_message_payload(content),
|
|
1200
|
+
}
|
|
1201
|
+
# Only forward non-None routing keys: when direct_messages_topic_id is
|
|
1202
|
+
# present _thread_kwargs_for_send pairs it with message_thread_id=None,
|
|
1203
|
+
# which must not be sent as a stray field on the raw endpoint.
|
|
1204
|
+
payload.update({k: v for k, v in thread_kwargs.items() if v is not None})
|
|
1205
|
+
payload.update(self._notification_kwargs(metadata))
|
|
1206
|
+
if getattr(self, "_disable_link_previews", False):
|
|
1207
|
+
payload["link_preview_options"] = {"is_disabled": True}
|
|
1208
|
+
if reply_to_id is not None:
|
|
1209
|
+
# Spec: sendRichMessage takes reply_parameters (ReplyParameters
|
|
1210
|
+
# object), NOT the legacy reply_to_message_id scalar. Unknown
|
|
1211
|
+
# params are silently ignored by the Bot API, so the scalar would
|
|
1212
|
+
# quietly drop the reply anchor instead of erroring.
|
|
1213
|
+
payload["reply_parameters"] = {"message_id": reply_to_id}
|
|
1214
|
+
|
|
1215
|
+
try:
|
|
1216
|
+
# Take the raw Bot API result (dict under real PTB). Passing
|
|
1217
|
+
# return_type=Message would make PTB deserialize a Bot API 10.1
|
|
1218
|
+
# response shape it does not fully model yet; a post-delivery parse
|
|
1219
|
+
# error must not be mistaken for a sendable failure.
|
|
1220
|
+
msg = await self._bot.do_api_request(
|
|
1221
|
+
"sendRichMessage", api_kwargs=payload
|
|
1222
|
+
)
|
|
1223
|
+
except Exception as exc:
|
|
1224
|
+
if self._is_rich_fallback_error(exc):
|
|
1225
|
+
if self._is_rich_capability_error(exc):
|
|
1226
|
+
# Endpoint missing (old PTB/server) — latch rich off so
|
|
1227
|
+
# every later send doesn't pay a doomed extra roundtrip.
|
|
1228
|
+
self._rich_send_disabled = True
|
|
1229
|
+
logger.debug(
|
|
1230
|
+
"[%s] sendRichMessage rejected (%s) — falling back to MarkdownV2",
|
|
1231
|
+
self.name, exc,
|
|
1232
|
+
)
|
|
1233
|
+
return None
|
|
1234
|
+
# Transient / network / unknown: the request may have reached
|
|
1235
|
+
# Telegram. Do NOT legacy-resend (duplicate risk); surface a
|
|
1236
|
+
# failure with retry semantics mirroring the legacy send() except.
|
|
1237
|
+
err_str = str(exc).lower()
|
|
1238
|
+
try:
|
|
1239
|
+
from telegram.error import TimedOut as _TimedOut
|
|
1240
|
+
except (ImportError, AttributeError):
|
|
1241
|
+
_TimedOut = None
|
|
1242
|
+
is_timeout = (_TimedOut and isinstance(exc, _TimedOut)) or "timed out" in err_str
|
|
1243
|
+
is_connect_timeout = self._looks_like_connect_timeout(exc)
|
|
1244
|
+
logger.warning(
|
|
1245
|
+
"[%s] sendRichMessage transient failure (no legacy resend): %s",
|
|
1246
|
+
self.name, exc,
|
|
1247
|
+
)
|
|
1248
|
+
return SendResult(
|
|
1249
|
+
success=False,
|
|
1250
|
+
error=str(exc),
|
|
1251
|
+
retryable=(is_connect_timeout or not is_timeout),
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
message_id = None
|
|
1255
|
+
if isinstance(msg, dict):
|
|
1256
|
+
message_id = msg.get("message_id")
|
|
1257
|
+
if message_id is None:
|
|
1258
|
+
message_id = (msg.get("result") or {}).get("message_id")
|
|
1259
|
+
else:
|
|
1260
|
+
message_id = getattr(msg, "message_id", None)
|
|
1261
|
+
if message_id is not None:
|
|
1262
|
+
# Telegram won't echo rich content in reply_to_message, so remember
|
|
1263
|
+
# what we sent — replies to this message resolve via this index.
|
|
1264
|
+
try:
|
|
1265
|
+
from gateway import rich_sent_store
|
|
1266
|
+
rich_sent_store.record(str(chat_id), str(message_id), content)
|
|
1267
|
+
except Exception:
|
|
1268
|
+
pass
|
|
1269
|
+
return SendResult(
|
|
1270
|
+
success=True,
|
|
1271
|
+
message_id=str(message_id) if message_id is not None else None,
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
async def _try_edit_rich(
|
|
1275
|
+
self,
|
|
1276
|
+
chat_id: str,
|
|
1277
|
+
message_id: str,
|
|
1278
|
+
content: str,
|
|
1279
|
+
) -> Optional[SendResult]:
|
|
1280
|
+
"""Edit an existing message in place as a rich message (Bot API 10.1).
|
|
1281
|
+
|
|
1282
|
+
Uses ``editMessageText`` with the ``rich_message`` parameter so a
|
|
1283
|
+
streamed preview can finalize as rich (tables/task lists/details/math)
|
|
1284
|
+
WITHOUT a fresh send + delete — no duplicate preview. Mirrors
|
|
1285
|
+
:meth:`_try_send_rich`'s error contract:
|
|
1286
|
+
|
|
1287
|
+
- success → ``SendResult(success=True, message_id=...)``
|
|
1288
|
+
- permanent / capability error → ``None`` (caller falls back to the
|
|
1289
|
+
legacy MarkdownV2 edit; capability errors latch rich off)
|
|
1290
|
+
- transient / unknown → ``SendResult(success=False)`` with retry
|
|
1291
|
+
semantics (the message may already be edited; do NOT legacy-resend)
|
|
1292
|
+
"""
|
|
1293
|
+
payload: Dict[str, Any] = {
|
|
1294
|
+
"chat_id": int(chat_id),
|
|
1295
|
+
"message_id": int(message_id),
|
|
1296
|
+
"rich_message": self._rich_message_payload(content),
|
|
1297
|
+
}
|
|
1298
|
+
if getattr(self, "_disable_link_previews", False):
|
|
1299
|
+
payload["link_preview_options"] = {"is_disabled": True}
|
|
1300
|
+
try:
|
|
1301
|
+
# Raw Bot API result; do not request return_type=Message (PTB does
|
|
1302
|
+
# not fully model the 10.1 response shape yet — a post-edit parse
|
|
1303
|
+
# error must not be mistaken for a failed edit).
|
|
1304
|
+
await self._bot.do_api_request("editMessageText", api_kwargs=payload)
|
|
1305
|
+
except Exception as exc:
|
|
1306
|
+
if self._is_rich_fallback_error(exc):
|
|
1307
|
+
if self._is_rich_capability_error(exc):
|
|
1308
|
+
self._rich_send_disabled = True
|
|
1309
|
+
# "Message is not modified" — content identical to the current
|
|
1310
|
+
# rich message; treat as a successful no-op so the caller does
|
|
1311
|
+
# not fall through to a redundant legacy edit.
|
|
1312
|
+
if "not modified" in str(exc).lower():
|
|
1313
|
+
return SendResult(success=True, message_id=message_id)
|
|
1314
|
+
logger.debug(
|
|
1315
|
+
"[%s] rich editMessageText rejected (%s) — falling back to MarkdownV2 edit",
|
|
1316
|
+
self.name, exc,
|
|
1317
|
+
)
|
|
1318
|
+
return None
|
|
1319
|
+
if "not modified" in str(exc).lower():
|
|
1320
|
+
return SendResult(success=True, message_id=message_id)
|
|
1321
|
+
err_str = str(exc).lower()
|
|
1322
|
+
try:
|
|
1323
|
+
from telegram.error import TimedOut as _TimedOut
|
|
1324
|
+
except (ImportError, AttributeError):
|
|
1325
|
+
_TimedOut = None
|
|
1326
|
+
is_timeout = (_TimedOut and isinstance(exc, _TimedOut)) or "timed out" in err_str
|
|
1327
|
+
is_connect_timeout = self._looks_like_connect_timeout(exc)
|
|
1328
|
+
logger.warning(
|
|
1329
|
+
"[%s] rich editMessageText transient failure (no legacy resend): %s",
|
|
1330
|
+
self.name, exc,
|
|
1331
|
+
)
|
|
1332
|
+
return SendResult(
|
|
1333
|
+
success=False,
|
|
1334
|
+
error=str(exc),
|
|
1335
|
+
retryable=(is_connect_timeout or not is_timeout),
|
|
1336
|
+
)
|
|
1337
|
+
return SendResult(success=True, message_id=message_id)
|
|
1338
|
+
|
|
1339
|
+
def _should_attempt_rich_draft(self, content: str) -> bool:
|
|
1340
|
+
return bool(
|
|
1341
|
+
getattr(self, "_rich_messages_enabled", True)
|
|
1342
|
+
and not getattr(self, "_rich_send_disabled", False)
|
|
1343
|
+
and not getattr(self, "_rich_draft_disabled", False)
|
|
1344
|
+
and content
|
|
1345
|
+
and content.strip()
|
|
1346
|
+
and not self._has_telegram_desktop_details_math_crash_shape(content)
|
|
1347
|
+
and self._content_fits_rich_limits(content)
|
|
1348
|
+
and self._bot_supports_rich()
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
async def _try_send_rich_draft(
|
|
1352
|
+
self,
|
|
1353
|
+
chat_id: str,
|
|
1354
|
+
draft_id: int,
|
|
1355
|
+
content: str,
|
|
1356
|
+
metadata: Optional[Dict[str, Any]],
|
|
1357
|
+
) -> bool:
|
|
1358
|
+
"""Emit one ``sendRichMessageDraft`` preview frame; True on success.
|
|
1359
|
+
|
|
1360
|
+
Draft frames are ephemeral and overwritten by the next frame / the
|
|
1361
|
+
final ``sendRichMessage``, so a duplicate or lost rich draft is
|
|
1362
|
+
harmless — any failure simply returns False and the caller renders the
|
|
1363
|
+
legacy plain-text draft. A permanent/capability failure additionally
|
|
1364
|
+
latches ``_rich_draft_disabled`` so later frames skip the rich attempt.
|
|
1365
|
+
"""
|
|
1366
|
+
payload: Dict[str, Any] = {
|
|
1367
|
+
"chat_id": int(chat_id),
|
|
1368
|
+
"draft_id": int(draft_id),
|
|
1369
|
+
"rich_message": self._rich_message_payload(content),
|
|
1370
|
+
}
|
|
1371
|
+
thread_id = self._metadata_thread_id(metadata)
|
|
1372
|
+
if thread_id is not None:
|
|
1373
|
+
payload["message_thread_id"] = int(thread_id)
|
|
1374
|
+
try:
|
|
1375
|
+
ok = await self._bot.do_api_request("sendRichMessageDraft", api_kwargs=payload)
|
|
1376
|
+
return bool(ok)
|
|
1377
|
+
except Exception as exc:
|
|
1378
|
+
if self._is_rich_capability_error(exc):
|
|
1379
|
+
self._rich_draft_disabled = True
|
|
1380
|
+
logger.debug(
|
|
1381
|
+
"[%s] sendRichMessageDraft unsupported (%s) — using legacy drafts",
|
|
1382
|
+
self.name, exc,
|
|
1383
|
+
)
|
|
1384
|
+
else:
|
|
1385
|
+
logger.debug(
|
|
1386
|
+
"[%s] sendRichMessageDraft transient failure (%s) — legacy draft this frame",
|
|
1387
|
+
self.name, exc,
|
|
1388
|
+
)
|
|
1389
|
+
return False
|
|
1390
|
+
|
|
902
1391
|
async def _drain_polling_connections(self) -> None:
|
|
903
1392
|
"""Reset the httpx connection pool used for getUpdates polling.
|
|
904
1393
|
|
|
@@ -1142,7 +1631,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
1142
1631
|
# gateway process is alive and reports "connected" but
|
|
1143
1632
|
# no messages are received or sent.
|
|
1144
1633
|
if self._polling_conflict_count < MAX_CONFLICT_RETRIES:
|
|
1145
|
-
loop
|
|
1634
|
+
# We are inside a running coroutine, so the running loop is
|
|
1635
|
+
# guaranteed to exist. asyncio.get_event_loop() is deprecated
|
|
1636
|
+
# and raises "RuntimeError: There is no current event loop in
|
|
1637
|
+
# thread 'MainThread'" on Python 3.10+ when invoked from a
|
|
1638
|
+
# context without an attached loop (which can happen when PTB
|
|
1639
|
+
# dispatches this error callback). Use get_running_loop().
|
|
1640
|
+
loop = asyncio.get_running_loop()
|
|
1146
1641
|
self._polling_error_task = loop.create_task(
|
|
1147
1642
|
self._handle_polling_conflict(retry_err)
|
|
1148
1643
|
)
|
|
@@ -1767,6 +2262,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
1767
2262
|
mode = "webhook" if self._webhook_mode else "polling"
|
|
1768
2263
|
logger.info("[%s] Connected to Telegram (%s mode)", self.name, mode)
|
|
1769
2264
|
|
|
2265
|
+
# Surface the gateway as "Online" in the bot's short description
|
|
2266
|
+
# (opt-in via extra.status_indicator). Non-fatal.
|
|
2267
|
+
try:
|
|
2268
|
+
await self._set_status_indicator(online=True)
|
|
2269
|
+
except Exception:
|
|
2270
|
+
pass
|
|
2271
|
+
|
|
1770
2272
|
# Set up DM topics (Bot API 9.4 — Private Chat Topics)
|
|
1771
2273
|
# Runs after connection is established so the bot can call createForumTopic.
|
|
1772
2274
|
# Failures here are non-fatal — the bot works fine without topics.
|
|
@@ -1787,8 +2289,47 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
1787
2289
|
logger.error("[%s] Failed to connect to Telegram: %s", self.name, e, exc_info=True)
|
|
1788
2290
|
return False
|
|
1789
2291
|
|
|
2292
|
+
async def _set_status_indicator(self, online: bool) -> None:
|
|
2293
|
+
"""Set the bot's short description to the online/offline status text.
|
|
2294
|
+
|
|
2295
|
+
The short description is the line shown under the bot's name in its
|
|
2296
|
+
profile. It is the closest Bot API surface to a presence indicator —
|
|
2297
|
+
bots have no real online/offline dot (that's a user-account feature).
|
|
2298
|
+
|
|
2299
|
+
No-op unless ``extra.status_indicator`` is enabled. Best-effort: any
|
|
2300
|
+
failure is logged at debug and swallowed so it never blocks connect or
|
|
2301
|
+
disconnect. The default (no language_code) description applies to every
|
|
2302
|
+
user who doesn't have a language-specific one set.
|
|
2303
|
+
"""
|
|
2304
|
+
if not getattr(self, "_status_indicator_enabled", False):
|
|
2305
|
+
return
|
|
2306
|
+
bot = self._bot
|
|
2307
|
+
if bot is None:
|
|
2308
|
+
return
|
|
2309
|
+
text = self._status_online_text if online else self._status_offline_text
|
|
2310
|
+
# Telegram caps short_description at 120 chars.
|
|
2311
|
+
text = text[:120]
|
|
2312
|
+
try:
|
|
2313
|
+
await bot.set_my_short_description(short_description=text)
|
|
2314
|
+
logger.info("[%s] Set bot status indicator to %r", self.name, text)
|
|
2315
|
+
except Exception as e:
|
|
2316
|
+
logger.debug(
|
|
2317
|
+
"[%s] Failed to set bot status indicator to %r: %s",
|
|
2318
|
+
self.name, text, e,
|
|
2319
|
+
)
|
|
2320
|
+
|
|
1790
2321
|
async def disconnect(self) -> None:
|
|
1791
2322
|
"""Stop polling/webhook, cancel pending album flushes, and disconnect."""
|
|
2323
|
+
# Mark the bot "Offline" in its short description while the bot's HTTP
|
|
2324
|
+
# client is still alive (before app shutdown closes it). Opt-in via
|
|
2325
|
+
# extra.status_indicator. Non-fatal. This is the clean-shutdown path;
|
|
2326
|
+
# a hard crash leaves the last-known status, which is the expected
|
|
2327
|
+
# limitation of a profile-text indicator.
|
|
2328
|
+
try:
|
|
2329
|
+
await self._set_status_indicator(online=False)
|
|
2330
|
+
except Exception:
|
|
2331
|
+
pass
|
|
2332
|
+
|
|
1792
2333
|
pending_media_group_tasks = list(self._media_group_tasks.values())
|
|
1793
2334
|
for task in pending_media_group_tasks:
|
|
1794
2335
|
task.cancel()
|
|
@@ -1860,6 +2401,22 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
1860
2401
|
return SendResult(success=True, message_id=None)
|
|
1861
2402
|
|
|
1862
2403
|
try:
|
|
2404
|
+
# Bot API 10.1 rich fast-path: send the raw agent markdown via
|
|
2405
|
+
# sendRichMessage so tables/task lists/etc. render natively. Falls
|
|
2406
|
+
# through to the legacy MarkdownV2 path on permanent/capability
|
|
2407
|
+
# errors or DM-topic routing skips; returns directly on success or
|
|
2408
|
+
# on a transient failure (which must NOT be legacy-resent).
|
|
2409
|
+
if self._should_attempt_rich(content, metadata=metadata):
|
|
2410
|
+
rich_result = await self._try_send_rich(chat_id, content, reply_to, metadata)
|
|
2411
|
+
if rich_result is not None:
|
|
2412
|
+
if rich_result.success:
|
|
2413
|
+
# Re-trigger typing like the legacy success path does.
|
|
2414
|
+
try:
|
|
2415
|
+
await self.send_typing(chat_id, metadata=metadata)
|
|
2416
|
+
except Exception:
|
|
2417
|
+
pass # Typing failures are non-fatal
|
|
2418
|
+
return rich_result
|
|
2419
|
+
|
|
1863
2420
|
# Format and split message if needed
|
|
1864
2421
|
formatted = self.format_message(content)
|
|
1865
2422
|
chunks = self.truncate_message(
|
|
@@ -2173,6 +2730,21 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2173
2730
|
if not self._bot:
|
|
2174
2731
|
return SendResult(success=False, error="Not connected")
|
|
2175
2732
|
|
|
2733
|
+
# Rich finalize (Bot API 10.1): when the completed content has
|
|
2734
|
+
# constructs the legacy MarkdownV2 edit degrades (tables → bullet
|
|
2735
|
+
# lists, task lists, <details>, block math) and rich is available,
|
|
2736
|
+
# edit the preview IN PLACE via editMessageText's rich_message param.
|
|
2737
|
+
# No fresh send + delete → no duplicate preview (the problem #46206
|
|
2738
|
+
# reverted the fresh-final path for). Attempted before the 4,096
|
|
2739
|
+
# overflow pre-flight because the rich text cap is 32,768 — a rich
|
|
2740
|
+
# table that exceeds the MarkdownV2 limit must not be split into legacy
|
|
2741
|
+
# chunks. Falls back to the legacy edit path (overflow split included)
|
|
2742
|
+
# on capability/permanent rejection.
|
|
2743
|
+
if finalize and self._rich_eligible(content):
|
|
2744
|
+
rich_result = await self._try_edit_rich(chat_id, message_id, content)
|
|
2745
|
+
if rich_result is not None:
|
|
2746
|
+
return rich_result
|
|
2747
|
+
|
|
2176
2748
|
# Pre-flight: if content already exceeds the limit, split-and-deliver
|
|
2177
2749
|
# without round-tripping a doomed edit.
|
|
2178
2750
|
if utf16_len(content) > self.MAX_MESSAGE_LENGTH:
|
|
@@ -2201,11 +2773,17 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2201
2773
|
# "Message is not modified" is a no-op, not an error
|
|
2202
2774
|
if "not modified" in str(fmt_err).lower():
|
|
2203
2775
|
return SendResult(success=True, message_id=message_id)
|
|
2204
|
-
# Fallback: retry
|
|
2776
|
+
# Fallback: strip MarkdownV2 escapes and retry as clean plain text
|
|
2777
|
+
logger.warning(
|
|
2778
|
+
"[%s] MarkdownV2 edit failed, falling back to plain text: %s",
|
|
2779
|
+
self.name,
|
|
2780
|
+
fmt_err,
|
|
2781
|
+
)
|
|
2782
|
+
_plain = _strip_mdv2(content) if content else content
|
|
2205
2783
|
await self._bot.edit_message_text(
|
|
2206
2784
|
chat_id=int(chat_id),
|
|
2207
2785
|
message_id=int(message_id),
|
|
2208
|
-
text=
|
|
2786
|
+
text=_plain,
|
|
2209
2787
|
)
|
|
2210
2788
|
return SendResult(success=True, message_id=message_id)
|
|
2211
2789
|
except Exception as e:
|
|
@@ -2333,10 +2911,15 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2333
2911
|
)
|
|
2334
2912
|
except Exception as fmt_err:
|
|
2335
2913
|
if "not modified" not in str(fmt_err).lower():
|
|
2914
|
+
logger.warning(
|
|
2915
|
+
"[%s] Overflow split: MarkdownV2 first-chunk edit "
|
|
2916
|
+
"failed, falling back to plain text: %s",
|
|
2917
|
+
self.name, fmt_err,
|
|
2918
|
+
)
|
|
2336
2919
|
await self._bot.edit_message_text(
|
|
2337
2920
|
chat_id=int(chat_id),
|
|
2338
2921
|
message_id=int(message_id),
|
|
2339
|
-
text=first_chunk,
|
|
2922
|
+
text=_strip_mdv2(first_chunk),
|
|
2340
2923
|
)
|
|
2341
2924
|
else:
|
|
2342
2925
|
await self._bot.edit_message_text(
|
|
@@ -2364,6 +2947,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2364
2947
|
# are already correctly sized). Best-effort MarkdownV2 with plain
|
|
2365
2948
|
# fallback, mirroring send().
|
|
2366
2949
|
continuation_ids: list[str] = []
|
|
2950
|
+
delivered_chunks = [first_chunk]
|
|
2367
2951
|
prev_id = message_id
|
|
2368
2952
|
thread_id = self._metadata_thread_id(metadata)
|
|
2369
2953
|
for chunk in chunks[1:]:
|
|
@@ -2377,7 +2961,14 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2377
2961
|
)
|
|
2378
2962
|
for use_markdown in (True, False) if finalize else (False,):
|
|
2379
2963
|
try:
|
|
2380
|
-
|
|
2964
|
+
if use_markdown:
|
|
2965
|
+
text = self.format_message(chunk)
|
|
2966
|
+
else:
|
|
2967
|
+
# Plain attempt: on finalize the MarkdownV2 attempt
|
|
2968
|
+
# failed, so degrade to clean stripped text, never
|
|
2969
|
+
# the raw chunk (raw ** / ``` markers would render
|
|
2970
|
+
# literally); streaming previews stay raw.
|
|
2971
|
+
text = _strip_mdv2(chunk) if finalize else chunk
|
|
2381
2972
|
sent_msg = await self._bot.send_message(
|
|
2382
2973
|
chat_id=int(chat_id),
|
|
2383
2974
|
text=text,
|
|
@@ -2403,7 +2994,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2403
2994
|
try:
|
|
2404
2995
|
sent_msg = await self._bot.send_message(
|
|
2405
2996
|
chat_id=int(chat_id),
|
|
2406
|
-
text=chunk,
|
|
2997
|
+
text=_strip_mdv2(chunk) if finalize else chunk,
|
|
2407
2998
|
**retry_thread_kwargs,
|
|
2408
2999
|
**self._link_preview_kwargs(),
|
|
2409
3000
|
**self._notification_kwargs(metadata),
|
|
@@ -2427,17 +3018,37 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2427
3018
|
break
|
|
2428
3019
|
if sent_msg is None:
|
|
2429
3020
|
# Continuation failed — the user has chunk 1 + however many
|
|
2430
|
-
# continuations succeeded
|
|
2431
|
-
#
|
|
2432
|
-
#
|
|
2433
|
-
#
|
|
3021
|
+
# continuations succeeded, but NOT the full response. Do not
|
|
3022
|
+
# report success: the stream consumer treats a successful edit
|
|
3023
|
+
# as final delivery on got_done, which would suppress fallback
|
|
3024
|
+
# delivery and leave the Telegram topic clipped after the last
|
|
3025
|
+
# delivered chunk.
|
|
2434
3026
|
logger.warning(
|
|
2435
3027
|
"[%s] Overflow split: stopped at %d/%d chunks delivered",
|
|
2436
3028
|
self.name, 1 + len(continuation_ids), len(chunks),
|
|
2437
3029
|
)
|
|
2438
|
-
|
|
3030
|
+
delivered_prefix = "".join(
|
|
3031
|
+
re.sub(r" \(\d+/\d+\)$", "", delivered)
|
|
3032
|
+
for delivered in delivered_chunks
|
|
3033
|
+
)
|
|
3034
|
+
return SendResult(
|
|
3035
|
+
success=False,
|
|
3036
|
+
message_id=prev_id,
|
|
3037
|
+
error="overflow_continuation_failed",
|
|
3038
|
+
retryable=True,
|
|
3039
|
+
raw_response={
|
|
3040
|
+
"partial_overflow": True,
|
|
3041
|
+
"delivered_chunks": 1 + len(continuation_ids),
|
|
3042
|
+
"total_chunks": len(chunks),
|
|
3043
|
+
"last_message_id": prev_id,
|
|
3044
|
+
"delivered_prefix": delivered_prefix,
|
|
3045
|
+
"continuation_message_ids": tuple(continuation_ids),
|
|
3046
|
+
},
|
|
3047
|
+
continuation_message_ids=tuple(continuation_ids),
|
|
3048
|
+
)
|
|
2439
3049
|
new_id = str(getattr(sent_msg, "message_id", "")) or prev_id
|
|
2440
3050
|
continuation_ids.append(new_id)
|
|
3051
|
+
delivered_chunks.append(chunk)
|
|
2441
3052
|
prev_id = new_id
|
|
2442
3053
|
|
|
2443
3054
|
last_id = continuation_ids[-1] if continuation_ids else message_id
|
|
@@ -2502,17 +3113,30 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
2502
3113
|
content: str,
|
|
2503
3114
|
metadata: Optional[Dict[str, Any]] = None,
|
|
2504
3115
|
) -> SendResult:
|
|
2505
|
-
"""Stream a partial message via Telegram's native
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
``
|
|
3116
|
+
"""Stream a partial message via Telegram's native draft API.
|
|
3117
|
+
|
|
3118
|
+
Uses ``sendRichMessageDraft`` (Bot API 10.1) with the raw markdown when
|
|
3119
|
+
rich messages are enabled and supported, otherwise the plain-text
|
|
3120
|
+
``sendMessageDraft``. The Bot API animates the preview when the same
|
|
3121
|
+
``draft_id`` is reused across consecutive calls in the same chat. When
|
|
3122
|
+
the response finishes, the caller sends the final text via the normal
|
|
3123
|
+
``send`` path; the draft preview clears naturally on the client
|
|
3124
|
+
(Telegram has no Bot API to "promote" a draft to a real message — the
|
|
3125
|
+
final ``sendMessage``/``sendRichMessage`` is what the user receives in
|
|
3126
|
+
their history).
|
|
2513
3127
|
"""
|
|
2514
3128
|
if not self._bot:
|
|
2515
3129
|
return SendResult(success=False, error="not_connected")
|
|
3130
|
+
|
|
3131
|
+
# Rich draft fast-path (Bot API 10.1 sendRichMessageDraft): render the
|
|
3132
|
+
# streaming preview with the same raw markdown the final
|
|
3133
|
+
# sendRichMessage will persist, so the animated draft matches the final
|
|
3134
|
+
# message. Any failure degrades to the legacy plain-text draft below.
|
|
3135
|
+
if self._should_attempt_rich_draft(content):
|
|
3136
|
+
if await self._try_send_rich_draft(chat_id, draft_id, content, metadata):
|
|
3137
|
+
# Drafts have no message_id; report success without one.
|
|
3138
|
+
return SendResult(success=True, message_id=None)
|
|
3139
|
+
|
|
2516
3140
|
if not hasattr(self._bot, "send_message_draft"):
|
|
2517
3141
|
return SendResult(success=False, error="api_unavailable")
|
|
2518
3142
|
|
|
@@ -3015,7 +3639,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3015
3639
|
async def _handle_model_picker_callback(
|
|
3016
3640
|
self, query, data: str, chat_id: str
|
|
3017
3641
|
) -> None:
|
|
3018
|
-
"""Handle model picker inline keyboard callbacks (mp:/mm:/mb:/mx:/mg:)."""
|
|
3642
|
+
"""Handle model picker inline keyboard callbacks (mp:/mm:/mc:/mb:/mx:/mg:)."""
|
|
3019
3643
|
state = self._model_picker_state.get(chat_id)
|
|
3020
3644
|
if not state:
|
|
3021
3645
|
await query.answer(text="Picker expired — use /model again.")
|
|
@@ -3100,6 +3724,55 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3100
3724
|
)
|
|
3101
3725
|
await query.answer()
|
|
3102
3726
|
|
|
3727
|
+
elif data.startswith("mc:"):
|
|
3728
|
+
# --- Expensive model confirmed: perform the switch ---
|
|
3729
|
+
try:
|
|
3730
|
+
idx = int(data[3:])
|
|
3731
|
+
except ValueError:
|
|
3732
|
+
await query.answer(text="Invalid selection.")
|
|
3733
|
+
return
|
|
3734
|
+
|
|
3735
|
+
model_list = state.get("model_list", [])
|
|
3736
|
+
if idx < 0 or idx >= len(model_list):
|
|
3737
|
+
await query.answer(text="Invalid model index.")
|
|
3738
|
+
return
|
|
3739
|
+
|
|
3740
|
+
model_id = model_list[idx]
|
|
3741
|
+
provider_slug = state.get("selected_provider", "")
|
|
3742
|
+
callback = state.get("on_model_selected")
|
|
3743
|
+
|
|
3744
|
+
if not callback:
|
|
3745
|
+
await query.answer(text="Picker expired.")
|
|
3746
|
+
return
|
|
3747
|
+
|
|
3748
|
+
switch_failed = False
|
|
3749
|
+
try:
|
|
3750
|
+
result_text = await callback(chat_id, model_id, provider_slug)
|
|
3751
|
+
except Exception as exc:
|
|
3752
|
+
logger.error("Model picker switch failed: %s", exc)
|
|
3753
|
+
result_text = f"Error switching model: {exc}"
|
|
3754
|
+
switch_failed = True
|
|
3755
|
+
|
|
3756
|
+
try:
|
|
3757
|
+
await query.edit_message_text(
|
|
3758
|
+
text=self.format_message(result_text),
|
|
3759
|
+
parse_mode=ParseMode.MARKDOWN_V2,
|
|
3760
|
+
reply_markup=None,
|
|
3761
|
+
)
|
|
3762
|
+
except Exception:
|
|
3763
|
+
try:
|
|
3764
|
+
await query.edit_message_text(
|
|
3765
|
+
text=result_text,
|
|
3766
|
+
parse_mode=None,
|
|
3767
|
+
reply_markup=None,
|
|
3768
|
+
)
|
|
3769
|
+
except Exception:
|
|
3770
|
+
pass
|
|
3771
|
+
await query.answer(
|
|
3772
|
+
text="Switch failed." if switch_failed else "Model switched!"
|
|
3773
|
+
)
|
|
3774
|
+
self._model_picker_state.pop(chat_id, None)
|
|
3775
|
+
|
|
3103
3776
|
elif data.startswith("mm:"):
|
|
3104
3777
|
# --- Model selected: perform the switch ---
|
|
3105
3778
|
try:
|
|
@@ -3121,11 +3794,43 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3121
3794
|
await query.answer(text="Picker expired.")
|
|
3122
3795
|
return
|
|
3123
3796
|
|
|
3797
|
+
try:
|
|
3798
|
+
from hermes_cli.model_cost_guard import expensive_model_warning
|
|
3799
|
+
|
|
3800
|
+
# Pricing lookup can hit models.dev / a /models endpoint on a
|
|
3801
|
+
# cache miss — keep it off the event loop.
|
|
3802
|
+
warning = await asyncio.to_thread(
|
|
3803
|
+
expensive_model_warning,
|
|
3804
|
+
model_id,
|
|
3805
|
+
provider=provider_slug,
|
|
3806
|
+
)
|
|
3807
|
+
except Exception:
|
|
3808
|
+
warning = None
|
|
3809
|
+
if warning is not None:
|
|
3810
|
+
keyboard = InlineKeyboardMarkup([
|
|
3811
|
+
[InlineKeyboardButton("Switch anyway", callback_data=f"mc:{idx}")],
|
|
3812
|
+
[
|
|
3813
|
+
InlineKeyboardButton("◀ Back", callback_data="mb"),
|
|
3814
|
+
InlineKeyboardButton("✗ Cancel", callback_data="mx"),
|
|
3815
|
+
],
|
|
3816
|
+
])
|
|
3817
|
+
await query.edit_message_text(
|
|
3818
|
+
text=self.format_message(
|
|
3819
|
+
f"⚠ *Expensive Model Warning*\n\n{warning.message}"
|
|
3820
|
+
),
|
|
3821
|
+
parse_mode=ParseMode.MARKDOWN_V2,
|
|
3822
|
+
reply_markup=keyboard,
|
|
3823
|
+
)
|
|
3824
|
+
await query.answer(text="Confirm expensive model")
|
|
3825
|
+
return
|
|
3826
|
+
|
|
3827
|
+
switch_failed = False
|
|
3124
3828
|
try:
|
|
3125
3829
|
result_text = await callback(chat_id, model_id, provider_slug)
|
|
3126
3830
|
except Exception as exc:
|
|
3127
3831
|
logger.error("Model picker switch failed: %s", exc)
|
|
3128
3832
|
result_text = f"Error switching model: {exc}"
|
|
3833
|
+
switch_failed = True
|
|
3129
3834
|
|
|
3130
3835
|
# Edit message to show confirmation, remove buttons
|
|
3131
3836
|
try:
|
|
@@ -3144,7 +3849,9 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3144
3849
|
)
|
|
3145
3850
|
except Exception:
|
|
3146
3851
|
pass
|
|
3147
|
-
await query.answer(
|
|
3852
|
+
await query.answer(
|
|
3853
|
+
text="Switch failed." if switch_failed else "Model switched!"
|
|
3854
|
+
)
|
|
3148
3855
|
|
|
3149
3856
|
# Clean up state
|
|
3150
3857
|
self._model_picker_state.pop(chat_id, None)
|
|
@@ -3245,7 +3952,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3245
3952
|
query_user_name = getattr(query.from_user, "first_name", None)
|
|
3246
3953
|
|
|
3247
3954
|
# --- Model picker callbacks ---
|
|
3248
|
-
if data.startswith(("mp:", "mpg:", "mm:", "mb", "mx", "mg:")):
|
|
3955
|
+
if data.startswith(("mp:", "mpg:", "mm:", "mc:", "mb", "mx", "mg:")):
|
|
3249
3956
|
chat_id = str(query.message.chat_id) if query.message else None
|
|
3250
3957
|
if chat_id:
|
|
3251
3958
|
await self._handle_model_picker_callback(query, data, chat_id)
|
|
@@ -3706,6 +4413,33 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
3706
4413
|
)
|
|
3707
4414
|
return error
|
|
3708
4415
|
|
|
4416
|
+
def _telegram_media_too_large_note(self, label: str, file_size: Any, max_bytes: int) -> str:
|
|
4417
|
+
limit_mb = max(1, max_bytes // (1024 * 1024))
|
|
4418
|
+
try:
|
|
4419
|
+
size_mb = int(file_size or 0) / (1024 * 1024)
|
|
4420
|
+
size_text = f"{size_mb:.1f} MB"
|
|
4421
|
+
except (TypeError, ValueError):
|
|
4422
|
+
size_text = "unknown size"
|
|
4423
|
+
return (
|
|
4424
|
+
f"[Telegram {label} skipped: file size {size_text} exceeds the "
|
|
4425
|
+
f"{limit_mb} MB limit. Ask the user to send a shorter voice note "
|
|
4426
|
+
"or a smaller audio file.]"
|
|
4427
|
+
)
|
|
4428
|
+
|
|
4429
|
+
def _telegram_media_size_allowed(self, source: Any, label: str) -> tuple[bool, Optional[str]]:
|
|
4430
|
+
"""Validate Telegram media size before downloading into memory."""
|
|
4431
|
+
max_bytes = int(getattr(self, "_max_doc_bytes", 20 * 1024 * 1024) or 20 * 1024 * 1024)
|
|
4432
|
+
file_size = getattr(source, "file_size", None)
|
|
4433
|
+
try:
|
|
4434
|
+
size = int(file_size or 0)
|
|
4435
|
+
except (TypeError, ValueError):
|
|
4436
|
+
size = 0
|
|
4437
|
+
if size <= 0:
|
|
4438
|
+
return True, None
|
|
4439
|
+
if size <= max_bytes:
|
|
4440
|
+
return True, None
|
|
4441
|
+
return False, self._telegram_media_too_large_note(label, size, max_bytes)
|
|
4442
|
+
|
|
3709
4443
|
async def send_voice(
|
|
3710
4444
|
self,
|
|
3711
4445
|
chat_id: str,
|
|
@@ -4073,7 +4807,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
4073
4807
|
)
|
|
4074
4808
|
return SendResult(success=True, message_id=str(msg.message_id))
|
|
4075
4809
|
except Exception as e:
|
|
4076
|
-
|
|
4810
|
+
logger.warning("[%s] Failed to send document: %s", self.name, e, exc_info=True)
|
|
4077
4811
|
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
|
|
4078
4812
|
|
|
4079
4813
|
async def send_video(
|
|
@@ -4120,7 +4854,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
4120
4854
|
)
|
|
4121
4855
|
return SendResult(success=True, message_id=str(msg.message_id))
|
|
4122
4856
|
except Exception as e:
|
|
4123
|
-
|
|
4857
|
+
logger.warning("[%s] Failed to send video: %s", self.name, e, exc_info=True)
|
|
4124
4858
|
return await super().send_video(chat_id, video_path, caption, reply_to, metadata=metadata)
|
|
4125
4859
|
|
|
4126
4860
|
async def send_image(
|
|
@@ -5009,6 +5743,52 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5009
5743
|
event.text = self._append_observed_note(event.text, cached.context_note())
|
|
5010
5744
|
logger.info("[Telegram] Cached observed group %s at %s", cached.kind, cached.path)
|
|
5011
5745
|
|
|
5746
|
+
async def _cache_replied_media(self, msg: Any, event: MessageEvent) -> None:
|
|
5747
|
+
"""Cache media from the message this turn replies to, if any."""
|
|
5748
|
+
from gateway.platforms.base import cache_media_bytes
|
|
5749
|
+
|
|
5750
|
+
reply_msg = getattr(msg, "reply_to_message", None)
|
|
5751
|
+
if reply_msg is None:
|
|
5752
|
+
return
|
|
5753
|
+
source, filename, mime, kind = self._observed_media_source(reply_msg)
|
|
5754
|
+
if source is None:
|
|
5755
|
+
return
|
|
5756
|
+
|
|
5757
|
+
max_bytes = getattr(self, "_max_doc_bytes", 20 * 1024 * 1024)
|
|
5758
|
+
file_size = getattr(source, "file_size", None)
|
|
5759
|
+
try:
|
|
5760
|
+
size = int(file_size or 0)
|
|
5761
|
+
except (TypeError, ValueError):
|
|
5762
|
+
size = 0
|
|
5763
|
+
if not (0 < size <= max_bytes):
|
|
5764
|
+
return
|
|
5765
|
+
|
|
5766
|
+
try:
|
|
5767
|
+
file_obj = await source.get_file()
|
|
5768
|
+
data = bytes(await file_obj.download_as_bytearray())
|
|
5769
|
+
if not filename:
|
|
5770
|
+
filename = os.path.basename(getattr(file_obj, "file_path", "") or "")
|
|
5771
|
+
cached = cache_media_bytes(data, filename=filename, mime_type=mime, default_kind=kind)
|
|
5772
|
+
except Exception as exc:
|
|
5773
|
+
logger.warning("[Telegram] Failed to cache replied-to media: %s", exc, exc_info=True)
|
|
5774
|
+
return
|
|
5775
|
+
|
|
5776
|
+
if cached is None:
|
|
5777
|
+
return
|
|
5778
|
+
|
|
5779
|
+
event.media_urls.append(cached.path)
|
|
5780
|
+
event.media_types.append(cached.media_type)
|
|
5781
|
+
if len(event.media_urls) == 1:
|
|
5782
|
+
if cached.kind == "image":
|
|
5783
|
+
event.message_type = MessageType.PHOTO
|
|
5784
|
+
elif cached.kind == "video":
|
|
5785
|
+
event.message_type = MessageType.VIDEO
|
|
5786
|
+
event.text = self._append_observed_note(
|
|
5787
|
+
event.text,
|
|
5788
|
+
f"[Replied-to {cached.kind} '{cached.display_name}' saved at: {cached.path}]",
|
|
5789
|
+
)
|
|
5790
|
+
logger.info("[Telegram] Cached replied-to %s at %s", cached.kind, cached.path)
|
|
5791
|
+
|
|
5012
5792
|
def _observed_media_source(self, msg: Message):
|
|
5013
5793
|
"""Return (telegram_file_source, filename, mime, default_kind) or Nones."""
|
|
5014
5794
|
if msg.photo:
|
|
@@ -5198,6 +5978,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5198
5978
|
|
|
5199
5979
|
event = self._build_message_event(msg, MessageType.TEXT, update_id=update.update_id)
|
|
5200
5980
|
event.text = self._clean_bot_trigger_text(event.text)
|
|
5981
|
+
await self._cache_replied_media(msg, event)
|
|
5201
5982
|
event = self._apply_telegram_group_observe_attribution(event)
|
|
5202
5983
|
self._enqueue_text_event(event)
|
|
5203
5984
|
|
|
@@ -5212,6 +5993,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5212
5993
|
|
|
5213
5994
|
event = self._build_message_event(msg, MessageType.COMMAND, update_id=update.update_id)
|
|
5214
5995
|
event.text = self._clean_bot_trigger_text(event.text)
|
|
5996
|
+
await self._cache_replied_media(msg, event)
|
|
5215
5997
|
event = self._apply_telegram_group_observe_attribution(event)
|
|
5216
5998
|
await self.handle_message(event)
|
|
5217
5999
|
|
|
@@ -5471,6 +6253,12 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5471
6253
|
# Download voice/audio messages to cache for STT transcription
|
|
5472
6254
|
if msg.voice:
|
|
5473
6255
|
try:
|
|
6256
|
+
allowed, note = self._telegram_media_size_allowed(msg.voice, "voice message")
|
|
6257
|
+
if not allowed:
|
|
6258
|
+
event.text = self._append_observed_note(event.text, note or "")
|
|
6259
|
+
logger.info("[Telegram] Skipped oversized user voice (size=%s)", getattr(msg.voice, "file_size", None))
|
|
6260
|
+
await self.handle_message(event)
|
|
6261
|
+
return
|
|
5474
6262
|
file_obj = await msg.voice.get_file()
|
|
5475
6263
|
audio_bytes = await file_obj.download_as_bytearray()
|
|
5476
6264
|
cached_path = cache_audio_from_bytes(bytes(audio_bytes), ext=".ogg")
|
|
@@ -5481,6 +6269,12 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5481
6269
|
logger.warning("[Telegram] Failed to cache voice: %s", e, exc_info=True)
|
|
5482
6270
|
elif msg.audio:
|
|
5483
6271
|
try:
|
|
6272
|
+
allowed, note = self._telegram_media_size_allowed(msg.audio, "audio file")
|
|
6273
|
+
if not allowed:
|
|
6274
|
+
event.text = self._append_observed_note(event.text, note or "")
|
|
6275
|
+
logger.info("[Telegram] Skipped oversized user audio (size=%s)", getattr(msg.audio, "file_size", None))
|
|
6276
|
+
await self.handle_message(event)
|
|
6277
|
+
return
|
|
5484
6278
|
file_obj = await msg.audio.get_file()
|
|
5485
6279
|
audio_bytes = await file_obj.download_as_bytearray()
|
|
5486
6280
|
cached_path = cache_audio_from_bytes(bytes(audio_bytes), ext=".mp3")
|
|
@@ -5977,6 +6771,19 @@ class TelegramAdapter(BasePlatformAdapter):
|
|
|
5977
6771
|
or message.reply_to_message.caption
|
|
5978
6772
|
or None
|
|
5979
6773
|
)
|
|
6774
|
+
if not reply_to_text:
|
|
6775
|
+
# Rich messages (sendRichMessage — the launchd briefings and
|
|
6776
|
+
# the gateway's own rich finals) are NOT echoed with their
|
|
6777
|
+
# content in reply_to_message; Telegram sends no text,
|
|
6778
|
+
# caption, or api_kwargs for them. Recover the text we sent
|
|
6779
|
+
# from our local send-time index, keyed by message id.
|
|
6780
|
+
try:
|
|
6781
|
+
from gateway import rich_sent_store
|
|
6782
|
+
reply_to_text = rich_sent_store.lookup(
|
|
6783
|
+
str(chat.id), reply_to_id
|
|
6784
|
+
)
|
|
6785
|
+
except Exception:
|
|
6786
|
+
reply_to_text = None
|
|
5980
6787
|
|
|
5981
6788
|
# Per-channel/topic ephemeral prompt
|
|
5982
6789
|
from gateway.platforms.base import resolve_channel_prompt
|