painapple-code 1.0.0__py3-none-any.whl
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.
- painapple_code/__init__.py +30 -0
- painapple_code/__main__.py +18 -0
- painapple_code/_version.py +24 -0
- painapple_code/auth_middleware.py +930 -0
- painapple_code/cli/__init__.py +204 -0
- painapple_code/cli/deploy/__init__.py +15 -0
- painapple_code/cli/deploy/claude_seed.py +41 -0
- painapple_code/cli/deploy/config.py +260 -0
- painapple_code/cli/deploy/container.py +715 -0
- painapple_code/cli/deploy/launch.py +163 -0
- painapple_code/cli/deploy/runtime.py +223 -0
- painapple_code/cli/lifecycle_cmd.py +443 -0
- painapple_code/cli/list_cmd.py +572 -0
- painapple_code/cli/manage_cmd.py +670 -0
- painapple_code/cli/netinfo.py +125 -0
- painapple_code/cli/profiles.py +390 -0
- painapple_code/cli/serve_args.py +163 -0
- painapple_code/cli/serve_config.py +256 -0
- painapple_code/cli/setup_cmd.py +664 -0
- painapple_code/cli/ui.py +438 -0
- painapple_code/cost_analytics.py +879 -0
- painapple_code/data/models.defaults.yaml +18 -0
- painapple_code/data/models.yaml +18 -0
- painapple_code/data/presets.defaults.json +32 -0
- painapple_code/data/strings.yaml +2269 -0
- painapple_code/helpers.py +406 -0
- painapple_code/paths.py +1514 -0
- painapple_code/prompt_explorer.py +984 -0
- painapple_code/providers/__init__.py +224 -0
- painapple_code/providers/base.py +710 -0
- painapple_code/providers/claude/__init__.py +74 -0
- painapple_code/providers/claude/capabilities.py +218 -0
- painapple_code/providers/claude/errors.py +79 -0
- painapple_code/providers/claude/launch.py +50 -0
- painapple_code/providers/claude/summary.py +89 -0
- painapple_code/providers/claude/translate.py +74 -0
- painapple_code/providers/claude_sdk/__init__.py +139 -0
- painapple_code/providers/claude_sdk/driver.py +412 -0
- painapple_code/providers/codex/__init__.py +87 -0
- painapple_code/providers/codex/capabilities.py +321 -0
- painapple_code/providers/codex/errors.py +70 -0
- painapple_code/providers/codex/launch.py +142 -0
- painapple_code/providers/codex/summary.py +196 -0
- painapple_code/providers/codex/translate.py +204 -0
- painapple_code/providers/codex_app_server/__init__.py +100 -0
- painapple_code/providers/codex_app_server/capabilities.py +41 -0
- painapple_code/providers/codex_app_server/launch.py +202 -0
- painapple_code/providers/codex_app_server/summary.py +291 -0
- painapple_code/providers/codex_app_server/translate.py +263 -0
- painapple_code/providers/codex_app_server/transport.py +238 -0
- painapple_code/routes/__init__.py +0 -0
- painapple_code/routes/api_agents.py +441 -0
- painapple_code/routes/api_app.py +368 -0
- painapple_code/routes/api_app_commit_sections.py +127 -0
- painapple_code/routes/api_app_config.py +997 -0
- painapple_code/routes/api_app_session_prefs.py +217 -0
- painapple_code/routes/api_browser.py +690 -0
- painapple_code/routes/api_commands.py +374 -0
- painapple_code/routes/api_costs.py +128 -0
- painapple_code/routes/api_drafts.py +139 -0
- painapple_code/routes/api_exec.py +149 -0
- painapple_code/routes/api_files.py +542 -0
- painapple_code/routes/api_git.py +657 -0
- painapple_code/routes/api_logs.py +649 -0
- painapple_code/routes/api_plugins.py +275 -0
- painapple_code/routes/api_project_config.py +418 -0
- painapple_code/routes/api_prompts.py +231 -0
- painapple_code/routes/api_providers.py +53 -0
- painapple_code/routes/api_search.py +411 -0
- painapple_code/routes/api_session_stash.py +234 -0
- painapple_code/routes/api_session_welcome.py +151 -0
- painapple_code/routes/api_sessions.py +742 -0
- painapple_code/routes/api_shadow.py +323 -0
- painapple_code/routes/api_shadow_db.py +512 -0
- painapple_code/routes/api_shadow_files.py +332 -0
- painapple_code/routes/api_shadow_search.py +476 -0
- painapple_code/routes/api_skills.py +610 -0
- painapple_code/routes/api_tasks.py +147 -0
- painapple_code/routes/api_terminal.py +540 -0
- painapple_code/routes/api_upload.py +293 -0
- painapple_code/routes/api_viewer.py +229 -0
- painapple_code/routes/dependencies.py +109 -0
- painapple_code/routes/ws_chat.py +837 -0
- painapple_code/server.py +2208 -0
- painapple_code/server_logging.py +263 -0
- painapple_code/services/__init__.py +0 -0
- painapple_code/services/agent_session.py +2738 -0
- painapple_code/session_store.py +253 -0
- painapple_code/session_store_core.py +879 -0
- painapple_code/shadow_db.py +830 -0
- painapple_code/shadow_db_plans.py +369 -0
- painapple_code/shadow_db_queries.py +595 -0
- painapple_code/shadow_db_schema.py +676 -0
- painapple_code/shadow_git.py +1296 -0
- painapple_code/shadow_git_frontmatter.py +314 -0
- painapple_code/shadow_git_sections.py +520 -0
- painapple_code/shadow_git_summary.py +462 -0
- painapple_code/shadow_parser.py +560 -0
- painapple_code/static/css/00-variables.css +276 -0
- painapple_code/static/css/01-base.css +245 -0
- painapple_code/static/css/02-tooltips.css +36 -0
- painapple_code/static/css/09-left-rail.css +448 -0
- painapple_code/static/css/10-header.css +777 -0
- painapple_code/static/css/11-connection-bar.css +120 -0
- painapple_code/static/css/20-messages.css +750 -0
- painapple_code/static/css/21-tool-blocks.css +2340 -0
- painapple_code/static/css/22-edit-diff.css +619 -0
- painapple_code/static/css/23-thinking.css +1584 -0
- painapple_code/static/css/24-background-tasks.css +359 -0
- painapple_code/static/css/25-context-block.css +252 -0
- painapple_code/static/css/26-session-setup.css +144 -0
- painapple_code/static/css/30-markdown.css +854 -0
- painapple_code/static/css/40-input.css +2418 -0
- painapple_code/static/css/41-autocomplete.css +127 -0
- painapple_code/static/css/42-activity-strip.css +97 -0
- painapple_code/static/css/43-file-autocomplete.css +121 -0
- painapple_code/static/css/44-snippets-autocomplete.css +224 -0
- painapple_code/static/css/45-selection-bar.css +433 -0
- painapple_code/static/css/46-keyboard-bar.css +119 -0
- painapple_code/static/css/47-keyboard-longpress.css +116 -0
- painapple_code/static/css/47-token-profile.css +106 -0
- painapple_code/static/css/50-welcome.css +3095 -0
- painapple_code/static/css/51-modals.css +301 -0
- painapple_code/static/css/52-utilities.css +77 -0
- painapple_code/static/css/53-image-upload.css +456 -0
- painapple_code/static/css/54-todo-list.css +102 -0
- painapple_code/static/css/55-file-upload.css +113 -0
- painapple_code/static/css/56-chat-navigator.css +218 -0
- painapple_code/static/css/56-permission-card.css +332 -0
- painapple_code/static/css/56-plan-approval.css +204 -0
- painapple_code/static/css/56-question-form.css +602 -0
- painapple_code/static/css/57-swipe-indicator.css +95 -0
- painapple_code/static/css/58-session-families.css +627 -0
- painapple_code/static/css/59-about.css +196 -0
- painapple_code/static/css/61-log-explorer.css +723 -0
- painapple_code/static/css/62-json-tree.css +489 -0
- painapple_code/static/css/62-skills-widget.css +650 -0
- painapple_code/static/css/63-commands-widget.css +30 -0
- painapple_code/static/css/63-plugins-widget.css +174 -0
- painapple_code/static/css/63-terminal.css +516 -0
- painapple_code/static/css/64-search-files-widget.css +448 -0
- painapple_code/static/css/65-snippets-widget.css +426 -0
- painapple_code/static/css/66-active-sessions.css +979 -0
- painapple_code/static/css/66-agents-widget.css +265 -0
- painapple_code/static/css/66-file-explorer-widget.css +855 -0
- painapple_code/static/css/66-git-panel.css +791 -0
- painapple_code/static/css/66-history-explorer.css +1496 -0
- painapple_code/static/css/67-diff-viewer.css +602 -0
- painapple_code/static/css/67-editor-view.css +748 -0
- painapple_code/static/css/67-file-preview-widget.css +1873 -0
- painapple_code/static/css/68-browser-widget.css +337 -0
- painapple_code/static/css/68-compare-wizard.css +355 -0
- painapple_code/static/css/68-csv-preview.css +106 -0
- painapple_code/static/css/68-diff-preview.css +103 -0
- painapple_code/static/css/68-helpers-install-widget.css +491 -0
- painapple_code/static/css/68-image-annotate.css +346 -0
- painapple_code/static/css/68-terminal-view.css +98 -0
- painapple_code/static/css/68-uploads-widget.css +229 -0
- painapple_code/static/css/69-cost-analytics.css +537 -0
- painapple_code/static/css/70-config-panel.css +3736 -0
- painapple_code/static/css/70-excalidraw.css +136 -0
- painapple_code/static/css/70-prompt-explorer.css +890 -0
- painapple_code/static/css/71-chart.css +111 -0
- painapple_code/static/css/72-discussion-sidebar.css +678 -0
- painapple_code/static/css/74-debug-logs.css +656 -0
- painapple_code/static/css/80-open-dialog.css +146 -0
- painapple_code/static/css/80-quick-switcher.css +383 -0
- painapple_code/static/css/80-widget-system.css +1763 -0
- painapple_code/static/css/81-quick-actions.css +1020 -0
- painapple_code/static/css/82-context-popover.css +1233 -0
- painapple_code/static/css/83-context-menu.css +339 -0
- painapple_code/static/css/85-lazy-loading.css +102 -0
- painapple_code/static/css/86-zen-mode.css +708 -0
- painapple_code/static/css/87-grid-switcher.css +346 -0
- painapple_code/static/css/login.css +198 -0
- painapple_code/static/dist/app.bundle.js +7177 -0
- painapple_code/static/feature-triage.html +446 -0
- painapple_code/static/icons/apple-touch-icon.png +0 -0
- painapple_code/static/icons/favicon-32.png +0 -0
- painapple_code/static/icons/favicon.ico +0 -0
- painapple_code/static/icons/icon-128.png +0 -0
- painapple_code/static/icons/icon-144.png +0 -0
- painapple_code/static/icons/icon-152.png +0 -0
- painapple_code/static/icons/icon-180.png +0 -0
- painapple_code/static/icons/icon-192.png +0 -0
- painapple_code/static/icons/icon-384.png +0 -0
- painapple_code/static/icons/icon-512.png +0 -0
- painapple_code/static/icons/icon-72.png +0 -0
- painapple_code/static/icons/icon-96.png +0 -0
- painapple_code/static/js/action-delegate.js +187 -0
- painapple_code/static/js/activity-strip.js +193 -0
- painapple_code/static/js/app/context-menus.js +619 -0
- painapple_code/static/js/app/file-preview.js +163 -0
- painapple_code/static/js/app/init-ui.js +574 -0
- painapple_code/static/js/app/input-events.js +561 -0
- painapple_code/static/js/app/input-resize.js +313 -0
- painapple_code/static/js/app/message-actions.js +419 -0
- painapple_code/static/js/app/panels.js +210 -0
- painapple_code/static/js/app/rendering-delegators.js +246 -0
- painapple_code/static/js/app/server-loading.js +365 -0
- painapple_code/static/js/app/session-ops.js +287 -0
- painapple_code/static/js/app/session-switch.js +443 -0
- painapple_code/static/js/app/settings-helpers.js +280 -0
- painapple_code/static/js/app/startup-welcome.js +290 -0
- painapple_code/static/js/app-context.js +157 -0
- painapple_code/static/js/app.js +2134 -0
- painapple_code/static/js/auth-fetch.js +46 -0
- painapple_code/static/js/background-tasks.js +274 -0
- painapple_code/static/js/boot.js +73 -0
- painapple_code/static/js/caret-position.js +171 -0
- painapple_code/static/js/chat-navigator.js +710 -0
- painapple_code/static/js/chat-search.js +323 -0
- painapple_code/static/js/command-executor.js +176 -0
- painapple_code/static/js/command-store.js +497 -0
- painapple_code/static/js/components.js +1414 -0
- painapple_code/static/js/config.js +218 -0
- painapple_code/static/js/context-menu.js +740 -0
- painapple_code/static/js/controllers/chat-controller.js +4252 -0
- painapple_code/static/js/controllers/dialog-controller.js +217 -0
- painapple_code/static/js/controllers/tab-controller.js +1828 -0
- painapple_code/static/js/controllers/thinking-controller.js +915 -0
- painapple_code/static/js/diff-utils.js +486 -0
- painapple_code/static/js/drafts-pill.js +95 -0
- painapple_code/static/js/editor-view.js +815 -0
- painapple_code/static/js/effort-settings.js +430 -0
- painapple_code/static/js/feature-triage.js +811 -0
- painapple_code/static/js/file-autocomplete.js +516 -0
- painapple_code/static/js/file-tabs.js +123 -0
- painapple_code/static/js/gestures.js +448 -0
- painapple_code/static/js/grid-switcher.js +334 -0
- painapple_code/static/js/input-handler.js +1472 -0
- painapple_code/static/js/keyboard-bar.js +495 -0
- painapple_code/static/js/layout-switcher.js +109 -0
- painapple_code/static/js/linkify-utils.js +200 -0
- painapple_code/static/js/login.js +141 -0
- painapple_code/static/js/open-dialog.js +879 -0
- painapple_code/static/js/orphan-terminals.js +373 -0
- painapple_code/static/js/path-utils.js +179 -0
- painapple_code/static/js/perf-marks.js +102 -0
- painapple_code/static/js/permission-settings.js +475 -0
- painapple_code/static/js/pointer-input.js +59 -0
- painapple_code/static/js/preview/json-tree-menu.js +226 -0
- painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
- painapple_code/static/js/preview/json-tree.js +457 -0
- painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
- painapple_code/static/js/preview/preview-edit.js +367 -0
- painapple_code/static/js/preview/preview-events.js +361 -0
- painapple_code/static/js/preview/preview-history.js +567 -0
- painapple_code/static/js/preview/preview-inline-edit.js +869 -0
- painapple_code/static/js/preview/preview-poll.js +108 -0
- painapple_code/static/js/preview/preview-render.js +572 -0
- painapple_code/static/js/preview/preview-search.js +421 -0
- painapple_code/static/js/preview/preview-state.js +256 -0
- painapple_code/static/js/preview/preview-utils.js +251 -0
- painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
- painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
- painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
- painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
- painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
- painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
- painapple_code/static/js/preview-plugins/index.js +34 -0
- painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
- painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
- painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
- painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
- painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
- painapple_code/static/js/project-color-picker.js +110 -0
- painapple_code/static/js/project-colors.js +163 -0
- painapple_code/static/js/prompt-favorites.js +187 -0
- painapple_code/static/js/quick-actions-menu.js +1736 -0
- painapple_code/static/js/quick-actions-registry.js +1239 -0
- painapple_code/static/js/quick-switcher/controller.js +294 -0
- painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
- painapple_code/static/js/quick-switcher/index.js +30 -0
- painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
- painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
- painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
- painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
- painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
- painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
- painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
- painapple_code/static/js/quick-switcher/registry.js +69 -0
- painapple_code/static/js/quick-switcher/ui/item.js +54 -0
- painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
- painapple_code/static/js/recent-opens.js +40 -0
- painapple_code/static/js/scroll-manager.js +530 -0
- painapple_code/static/js/scroll-state-machine.js +359 -0
- painapple_code/static/js/selection/action-bar.js +793 -0
- painapple_code/static/js/selection/index.js +15 -0
- painapple_code/static/js/selection/selection-handler.js +877 -0
- painapple_code/static/js/selection/state.js +181 -0
- painapple_code/static/js/session/agent-progress.js +202 -0
- painapple_code/static/js/session/handle-agent-message.js +481 -0
- painapple_code/static/js/session/handle-message.js +550 -0
- painapple_code/static/js/session/interactive.js +587 -0
- painapple_code/static/js/session/message-store.js +271 -0
- painapple_code/static/js/session/messages.js +253 -0
- painapple_code/static/js/session/persistence.js +179 -0
- painapple_code/static/js/session/restore.js +136 -0
- painapple_code/static/js/session/sync.js +333 -0
- painapple_code/static/js/session-container-pool.js +371 -0
- painapple_code/static/js/session-setup-panel.js +274 -0
- painapple_code/static/js/session.js +1047 -0
- painapple_code/static/js/shortcut-hints.js +272 -0
- painapple_code/static/js/shortcuts.js +1146 -0
- painapple_code/static/js/skills-autocomplete.js +262 -0
- painapple_code/static/js/snippets-autocomplete.js +738 -0
- painapple_code/static/js/stash-refs-view.js +83 -0
- painapple_code/static/js/stash-ui.js +768 -0
- painapple_code/static/js/stash.js +785 -0
- painapple_code/static/js/status-bar.js +1184 -0
- painapple_code/static/js/token-profile.js +274 -0
- painapple_code/static/js/tool-renderer-blocks.js +1626 -0
- painapple_code/static/js/tool-renderer-thinking.js +813 -0
- painapple_code/static/js/tool-renderer.js +1500 -0
- painapple_code/static/js/tooltips.js +197 -0
- painapple_code/static/js/upload-manager.js +1019 -0
- painapple_code/static/js/utils.js +521 -0
- painapple_code/static/js/welcome/api.js +363 -0
- painapple_code/static/js/welcome/cards.js +626 -0
- painapple_code/static/js/welcome/context-menu.js +585 -0
- painapple_code/static/js/welcome/families.js +801 -0
- painapple_code/static/js/welcome/preview.js +234 -0
- painapple_code/static/js/welcome/state.js +129 -0
- painapple_code/static/js/welcome.js +1874 -0
- painapple_code/static/js/widget-system/base-widget.js +717 -0
- painapple_code/static/js/widget-system/device-manager.js +178 -0
- painapple_code/static/js/widget-system/event-bus.js +152 -0
- painapple_code/static/js/widget-system/icons.js +120 -0
- painapple_code/static/js/widget-system/index.js +49 -0
- painapple_code/static/js/widget-system/init.js +54 -0
- painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
- painapple_code/static/js/widget-system/types/floating.js +915 -0
- painapple_code/static/js/widget-system/types/index.js +40 -0
- painapple_code/static/js/widget-system/types/modal.js +147 -0
- painapple_code/static/js/widget-system/types/sidebar.js +158 -0
- painapple_code/static/js/widget-system/types/tab.js +146 -0
- painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
- painapple_code/static/js/widget-system/widget-manager.js +1187 -0
- painapple_code/static/js/widgets/about-widget.js +260 -0
- painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
- painapple_code/static/js/widgets/agents-widget.js +897 -0
- painapple_code/static/js/widgets/browser-widget.js +579 -0
- painapple_code/static/js/widgets/commands-widget.js +694 -0
- painapple_code/static/js/widgets/config/commit-sections.js +449 -0
- painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
- painapple_code/static/js/widgets/config/gestures.js +161 -0
- painapple_code/static/js/widgets/config/models-tab.js +988 -0
- painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
- painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
- painapple_code/static/js/widgets/config/state.js +517 -0
- painapple_code/static/js/widgets/config/system-controls.js +90 -0
- painapple_code/static/js/widgets/config-widget.js +1535 -0
- painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
- painapple_code/static/js/widgets/debug-widget.js +693 -0
- painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
- painapple_code/static/js/widgets/discussion-widget.js +1107 -0
- painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
- painapple_code/static/js/widgets/file-preview-widget.js +861 -0
- painapple_code/static/js/widgets/git-widget.js +794 -0
- painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
- painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
- painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
- painapple_code/static/js/widgets/image-preview-widget.js +692 -0
- painapple_code/static/js/widgets/index.js +176 -0
- painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
- painapple_code/static/js/widgets/plugins-widget.js +305 -0
- painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
- painapple_code/static/js/widgets/search-files-widget.js +529 -0
- painapple_code/static/js/widgets/skills-widget.js +843 -0
- painapple_code/static/js/widgets/snippets-widget.js +479 -0
- painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
- painapple_code/static/js/widgets/tasks-widget.js +379 -0
- painapple_code/static/js/widgets/terminal/connection.js +283 -0
- painapple_code/static/js/widgets/terminal/gestures.js +357 -0
- painapple_code/static/js/widgets/terminal/init.js +642 -0
- painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
- painapple_code/static/js/widgets/terminal/osc52.js +213 -0
- painapple_code/static/js/widgets/terminal/render.js +159 -0
- painapple_code/static/js/widgets/terminal/size.js +54 -0
- painapple_code/static/js/widgets/terminal/state.js +211 -0
- painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
- painapple_code/static/js/widgets/terminal-widget.js +793 -0
- painapple_code/static/js/widgets/uploads-widget.js +461 -0
- painapple_code/static/js/widgets/zen-widget.js +1001 -0
- painapple_code/static/login.html +51 -0
- painapple_code/static/manifest.json +73 -0
- painapple_code/static/sw.js +293 -0
- painapple_code/static/vendor/LICENSES.txt +769 -0
- painapple_code/static/vendor/README.md +103 -0
- painapple_code/static/vendor/codemirror.js +35 -0
- painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
- painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
- painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
- painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
- painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
- painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
- painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
- painapple_code/static/vendor/highlight.min.js +1213 -0
- painapple_code/static/vendor/marked.min.js +6 -0
- painapple_code/static/vendor/purify.min.js +3 -0
- painapple_code/static/vendor/xterm-addon-fit.js +2 -0
- painapple_code/static/vendor/xterm.css +209 -0
- painapple_code/static/vendor/xterm.js +2 -0
- painapple_code/static/web-client.html +635 -0
- painapple_code/subprocess_registry.py +535 -0
- painapple_code/tls_cert.py +79 -0
- painapple_code/tools/agents/shadow-git-helper.md +400 -0
- painapple_code/tools/excalidraw-to-svg.js +143 -0
- painapple_code/tools/install-helpers.sh +148 -0
- painapple_code/tools/shadow-git +320 -0
- painapple_code/tools/shadow-query +120 -0
- painapple_code/tools/vegalite-to-svg.js +79 -0
- painapple_code/turn_query.py +766 -0
- painapple_code/turn_tracker.py +221 -0
- painapple_code/utils/__init__.py +0 -0
- painapple_code/utils/agent_cli.py +306 -0
- painapple_code/utils/chart.py +103 -0
- painapple_code/utils/excalidraw.py +107 -0
- painapple_code/utils/file_lock.py +162 -0
- painapple_code/utils/file_paths.py +648 -0
- painapple_code/utils/generate_icons.py +251 -0
- painapple_code/utils/proc.py +205 -0
- painapple_code/utils/pty_backend.py +59 -0
- painapple_code/utils/pty_posix.py +160 -0
- painapple_code/utils/pty_windows.py +245 -0
- painapple_code/utils/token_profiles.py +91 -0
- painapple_code/viewer_templates.py +657 -0
- painapple_code/welcome_search.py +879 -0
- painapple_code-1.0.0.dist-info/METADATA +292 -0
- painapple_code-1.0.0.dist-info/RECORD +436 -0
- painapple_code-1.0.0.dist-info/WHEEL +5 -0
- painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
- painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
- painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
- painapple_code-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive question/approval mixin — Claude can pause for user input via
|
|
3
|
+
* AskUserQuestion (multi-question wizard) or ExitPlanMode (plan approval).
|
|
4
|
+
* This module owns:
|
|
5
|
+
* - handleAskUserQuestion: render new or merge into existing wizard
|
|
6
|
+
* - handleExitPlanMode: render plan-approval card with detected planFile
|
|
7
|
+
* - sendToolAnswer / sendToolAnswers: ship answers back over WebSocket and
|
|
8
|
+
* mark the message as answered
|
|
9
|
+
*
|
|
10
|
+
* Both render flows update the tab badge (?), scroll to the new card, and
|
|
11
|
+
* persist via sessionManager. Applied to Session.prototype via
|
|
12
|
+
* Object.assign in session.js.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { debug } from '../config.js';
|
|
16
|
+
import { genId } from '../utils.js';
|
|
17
|
+
import { showToast } from '../context-menu.js';
|
|
18
|
+
import S from '../strings.js';
|
|
19
|
+
import { basename } from '../path-utils.js';
|
|
20
|
+
|
|
21
|
+
const getApp = () => window.app;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Coerce a tool-input `questions` value into an array of question objects.
|
|
25
|
+
*
|
|
26
|
+
* The CLI/SDK usually delivers `input.questions` already parsed as an array,
|
|
27
|
+
* but some turns arrive with it as a JSON-encoded *string* (e.g.
|
|
28
|
+
* `"[{\"question\": ...}]"`). When that happens the old `Array.isArray` guard
|
|
29
|
+
* silently fell through to `[]`, so the "Claude is asking" card rendered with
|
|
30
|
+
* no questions and no options — visibly broken. Parse the string form here so
|
|
31
|
+
* both shapes work.
|
|
32
|
+
* @param {*} raw - block.input?.questions (array, JSON string, or undefined)
|
|
33
|
+
* @returns {Array} normalized questions array (empty if unparseable)
|
|
34
|
+
*/
|
|
35
|
+
function normalizeQuestions(raw) {
|
|
36
|
+
if (Array.isArray(raw)) return raw;
|
|
37
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (Array.isArray(parsed)) return parsed;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
debug.log('Failed to parse AskUserQuestion questions string:', e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const interactiveMethods = {
|
|
49
|
+
/**
|
|
50
|
+
* Handle AskUserQuestion tool - render interactive form for user input
|
|
51
|
+
* Groups multiple questions from the same turn into a wizard-style UI
|
|
52
|
+
* @param {Object} block - Tool use block with id, name, input
|
|
53
|
+
*/
|
|
54
|
+
handleAskUserQuestion(block) {
|
|
55
|
+
const app = getApp();
|
|
56
|
+
const timestamp = this._currentServerTimestamp || new Date().toISOString();
|
|
57
|
+
|
|
58
|
+
// Check if there's an existing unanswered question message to merge with
|
|
59
|
+
// (multiple AskUserQuestion calls in same turn should be grouped)
|
|
60
|
+
const existingQuestion = this.messages.find(m =>
|
|
61
|
+
m.role === 'question' && !m.answered
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Create entry for this tool call
|
|
65
|
+
const questions = normalizeQuestions(block.input?.questions);
|
|
66
|
+
const entry = {
|
|
67
|
+
toolId: block.id,
|
|
68
|
+
questions,
|
|
69
|
+
answers: {}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (existingQuestion) {
|
|
73
|
+
// Merge into existing question group
|
|
74
|
+
if (!existingQuestion.entries) {
|
|
75
|
+
// Convert single-entry format to multi-entry
|
|
76
|
+
existingQuestion.entries = [{
|
|
77
|
+
toolId: existingQuestion.toolId,
|
|
78
|
+
questions: Array.isArray(existingQuestion.questions) ? existingQuestion.questions : [],
|
|
79
|
+
answers: existingQuestion.answers || {}
|
|
80
|
+
}];
|
|
81
|
+
}
|
|
82
|
+
existingQuestion.entries.push(entry);
|
|
83
|
+
existingQuestion.timestamp = timestamp; // Update timestamp
|
|
84
|
+
|
|
85
|
+
debug.log('Merged AskUserQuestion into existing group:', block.id, 'total entries:', existingQuestion.entries.length);
|
|
86
|
+
|
|
87
|
+
// Re-render the message to show updated wizard
|
|
88
|
+
if (app.activeSession === this) {
|
|
89
|
+
const existingEl = document.getElementById(`msg-${existingQuestion.id}`);
|
|
90
|
+
if (existingEl) {
|
|
91
|
+
const newEl = app.chatCtrl.createMessageElement(existingQuestion);
|
|
92
|
+
existingEl.replaceWith(newEl);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Auto-dismiss any stale unanswered questions from previous turns
|
|
97
|
+
// (e.g., auto-denied AskUserQuestion that was never answered by user)
|
|
98
|
+
const staleQuestions = this.messages.filter(m => m.role === 'question' && !m.answered);
|
|
99
|
+
for (const stale of staleQuestions) {
|
|
100
|
+
stale.answered = true;
|
|
101
|
+
if (app.activeSession === this) {
|
|
102
|
+
const el = document.getElementById(`msg-${stale.id}`);
|
|
103
|
+
if (el) {
|
|
104
|
+
const newEl = app.chatCtrl.createMessageElement(stale);
|
|
105
|
+
el.replaceWith(newEl);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create new question message with entries array
|
|
111
|
+
const msg = {
|
|
112
|
+
id: genId(),
|
|
113
|
+
role: 'question',
|
|
114
|
+
toolId: block.id, // Keep for backward compat
|
|
115
|
+
toolName: 'AskUserQuestion',
|
|
116
|
+
questions, // Keep for backward compat
|
|
117
|
+
entries: [entry], // New grouped format
|
|
118
|
+
answered: false,
|
|
119
|
+
answers: {}, // Keep for backward compat
|
|
120
|
+
activeTab: 0, // Track active tab in wizard
|
|
121
|
+
timestamp
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
this.messages.push(msg);
|
|
125
|
+
|
|
126
|
+
if (app.activeSession === this) {
|
|
127
|
+
app.renderMessage(msg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
debug.log('Created new AskUserQuestion:', block.id, questions.length, 'questions');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.lastActivity = timestamp;
|
|
134
|
+
this.pendingQuestionId = block.id; // Track pending question
|
|
135
|
+
this.updateTab(); // Update tab badge (shows ? indicator)
|
|
136
|
+
|
|
137
|
+
// Scroll to the question form and update indicator
|
|
138
|
+
if (app.activeSession === this) {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const questionMsg = this.messages.find(m => m.role === 'question' && !m.answered);
|
|
141
|
+
if (questionMsg) {
|
|
142
|
+
const questionEl = document.getElementById(`msg-${questionMsg.id}`);
|
|
143
|
+
if (questionEl) {
|
|
144
|
+
questionEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Update indicator (will show if user scrolls away)
|
|
148
|
+
app.updateQuestionIndicator();
|
|
149
|
+
}, 100);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
app.sessionManager.saveSessions();
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle ExitPlanMode tool - render interactive plan approval UI
|
|
157
|
+
* @param {Object} block - Tool use block with id, name, input
|
|
158
|
+
*/
|
|
159
|
+
handleExitPlanMode(block) {
|
|
160
|
+
const app = getApp();
|
|
161
|
+
const timestamp = this._currentServerTimestamp || new Date().toISOString();
|
|
162
|
+
|
|
163
|
+
// Find the plan file from session's Write tool history
|
|
164
|
+
let planFile = null;
|
|
165
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
166
|
+
const m = this.messages[i];
|
|
167
|
+
if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path?.includes('.claude/plans/')) {
|
|
168
|
+
planFile = m.toolInput.file_path;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fallback: look for any recent Write with 'plan' in the filename
|
|
174
|
+
if (!planFile) {
|
|
175
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
176
|
+
const m = this.messages[i];
|
|
177
|
+
if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path) {
|
|
178
|
+
const fname = basename(m.toolInput.file_path).toLowerCase();
|
|
179
|
+
if (fname.includes('plan')) {
|
|
180
|
+
planFile = m.toolInput.file_path;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Final fallback: use the most recent Write tool output (the plan file
|
|
188
|
+
// may have a non-standard name like "modular-dreaming-riddle.md")
|
|
189
|
+
if (!planFile) {
|
|
190
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
191
|
+
const m = this.messages[i];
|
|
192
|
+
if (m.role === 'tool' && m.toolName === 'Write' && m.toolInput?.file_path) {
|
|
193
|
+
planFile = m.toolInput.file_path;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const msg = {
|
|
200
|
+
id: genId(),
|
|
201
|
+
role: 'plan_approval',
|
|
202
|
+
toolId: block.id,
|
|
203
|
+
toolName: 'ExitPlanMode',
|
|
204
|
+
planFile,
|
|
205
|
+
answered: false,
|
|
206
|
+
decision: null, // 'approve' or 'reject'
|
|
207
|
+
timestamp
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this.messages.push(msg);
|
|
211
|
+
|
|
212
|
+
if (app.activeSession === this) {
|
|
213
|
+
app.renderMessage(msg);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.lastActivity = timestamp;
|
|
217
|
+
this.pendingQuestionId = block.id; // Reuse ? badge mechanism
|
|
218
|
+
this.updateTab();
|
|
219
|
+
|
|
220
|
+
// Scroll to the approval card
|
|
221
|
+
if (app.activeSession === this) {
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
const el = document.getElementById(`msg-${msg.id}`);
|
|
224
|
+
if (el) {
|
|
225
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
226
|
+
}
|
|
227
|
+
app.updateQuestionIndicator();
|
|
228
|
+
}, 100);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
app.sessionManager.saveSessions();
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle a `permission_request` frame (interactive can_use_tool ask from
|
|
236
|
+
* the claude-sdk provider). Renders an inline approve/deny card; the
|
|
237
|
+
* provider process is paused until sendPermissionResponse() answers.
|
|
238
|
+
* Reconnect replays are deduped by requestId.
|
|
239
|
+
* @param {Object} frame - {request_id, tool_name, input, description, ...}
|
|
240
|
+
*/
|
|
241
|
+
handlePermissionRequest(frame) {
|
|
242
|
+
const app = getApp();
|
|
243
|
+
const timestamp = this._currentServerTimestamp || new Date().toISOString();
|
|
244
|
+
|
|
245
|
+
// Dedupe: reconnect replay of a card we already have. Only replay
|
|
246
|
+
// frames dedupe, and only against a still-unanswered card — request
|
|
247
|
+
// ids are per-process counters (perm-1, perm-2, …), so a NON-replay
|
|
248
|
+
// frame reusing an id is a NEW ask from a restarted process, not a
|
|
249
|
+
// duplicate. Matching any old card here would swallow that ask and
|
|
250
|
+
// leave the process blocked with no card to answer.
|
|
251
|
+
if (frame.replay && this.messages.some(m =>
|
|
252
|
+
m.role === 'permission' && !m.answered && m.requestId === frame.request_id)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A non-replay ask reusing an unanswered card's request id means the
|
|
257
|
+
// process restarted (ids restart at perm-1 per process) — that old
|
|
258
|
+
// card's ask died with its process and can never be answered. Expire
|
|
259
|
+
// just the colliding card. Other unanswered cards are left alone:
|
|
260
|
+
// parallel tool calls from the live process legitimately stack asks,
|
|
261
|
+
// and they all must stay answerable. Cards orphaned by a process
|
|
262
|
+
// death are expired by expirePendingPermissionCards (session_ended)
|
|
263
|
+
// or lazily via the ok=false response ack.
|
|
264
|
+
if (!frame.replay) {
|
|
265
|
+
for (const stale of this.messages.filter(m =>
|
|
266
|
+
m.role === 'permission' && !m.answered && m.requestId === frame.request_id)) {
|
|
267
|
+
stale.answered = true;
|
|
268
|
+
stale.decision = 'expired';
|
|
269
|
+
stale.waitedMs = this._permWaitedMs(stale);
|
|
270
|
+
this._rerenderPermissionCard(stale);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const msg = {
|
|
275
|
+
id: genId(),
|
|
276
|
+
role: 'permission',
|
|
277
|
+
requestId: frame.request_id,
|
|
278
|
+
toolName: frame.tool_name,
|
|
279
|
+
toolInput: frame.input || {},
|
|
280
|
+
toolUseId: frame.tool_use_id || null,
|
|
281
|
+
title: frame.title || null,
|
|
282
|
+
description: frame.description || null,
|
|
283
|
+
// Engine "don't ask again" rule suggestions → "always allow" rows
|
|
284
|
+
suggestions: Array.isArray(frame.suggestions) ? frame.suggestions : [],
|
|
285
|
+
answered: false,
|
|
286
|
+
decision: null, // 'allow' | 'allow_always' | 'deny' | 'expired'
|
|
287
|
+
timestamp
|
|
288
|
+
};
|
|
289
|
+
this.messages.push(msg);
|
|
290
|
+
|
|
291
|
+
if (app.activeSession === this) {
|
|
292
|
+
app.renderMessage(msg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// The provider process is blocked in its permission callback — show a
|
|
296
|
+
// paused activity state and freeze the elapsed timer until the answer.
|
|
297
|
+
if (!this._permPausedAt) {
|
|
298
|
+
this._permPausedAt = Date.now();
|
|
299
|
+
this._activityBeforePermission = this._lastActivity;
|
|
300
|
+
}
|
|
301
|
+
this._setActivity({
|
|
302
|
+
active: true,
|
|
303
|
+
paused: true,
|
|
304
|
+
icon: 'lock',
|
|
305
|
+
label: S.permission_card.activity_paused,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
this.lastActivity = timestamp;
|
|
309
|
+
this.pendingQuestionId = frame.request_id; // Reuse ? badge mechanism
|
|
310
|
+
this.updateTab();
|
|
311
|
+
|
|
312
|
+
if (app.activeSession === this) {
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
const el = document.getElementById(`msg-${msg.id}`);
|
|
315
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
316
|
+
app.updateQuestionIndicator();
|
|
317
|
+
}, 100);
|
|
318
|
+
} else if (!frame.replay) {
|
|
319
|
+
// Background tab needs a decision — surface a click-to-go toast
|
|
320
|
+
const label = S.permission_card.toast_pending.replace('{name}', this.name || 'Session');
|
|
321
|
+
showToast(label, {
|
|
322
|
+
interactive: true,
|
|
323
|
+
duration: 10000,
|
|
324
|
+
className: 'permission-toast',
|
|
325
|
+
onMount: (toast) => {
|
|
326
|
+
toast.style.cursor = 'pointer';
|
|
327
|
+
toast.addEventListener('click', () => getApp().switchSession(this));
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
app.sessionManager.saveSessions();
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Send the user's allow/deny decision for a permission card.
|
|
337
|
+
* @param {string} msgId - The permission message id
|
|
338
|
+
* @param {string} behavior - 'allow' | 'deny'
|
|
339
|
+
* @param {string} [feedback] - Optional deny guidance for the model
|
|
340
|
+
* @param {number} [suggestionIndex] - "Always allow": index into the
|
|
341
|
+
* request's suggestions; the engine applies that rule update itself.
|
|
342
|
+
*/
|
|
343
|
+
sendPermissionResponse(msgId, behavior, feedback, suggestionIndex) {
|
|
344
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
345
|
+
console.error('Cannot send permission response: WebSocket not connected');
|
|
346
|
+
showToast(S.errors.not_connected);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
const msg = this.messages.find(m => m.role === 'permission' && m.id === msgId);
|
|
350
|
+
if (!msg || msg.answered) return false;
|
|
351
|
+
|
|
352
|
+
const alwaysAllow = behavior === 'allow' && Number.isInteger(suggestionIndex);
|
|
353
|
+
this.ws.send(JSON.stringify({
|
|
354
|
+
type: 'permission_response',
|
|
355
|
+
request_id: msg.requestId,
|
|
356
|
+
behavior,
|
|
357
|
+
message: feedback || undefined,
|
|
358
|
+
suggestion_index: alwaysAllow ? suggestionIndex : undefined,
|
|
359
|
+
}));
|
|
360
|
+
debug.log('Sent permission response:', msg.requestId, behavior, suggestionIndex);
|
|
361
|
+
|
|
362
|
+
msg.answered = true;
|
|
363
|
+
msg.decision = alwaysAllow ? 'allow_always' : behavior;
|
|
364
|
+
msg.waitedMs = this._permWaitedMs(msg);
|
|
365
|
+
if (behavior === 'deny' && feedback) msg.feedback = feedback;
|
|
366
|
+
this._rerenderPermissionCard(msg);
|
|
367
|
+
|
|
368
|
+
this.pendingQuestionId = null;
|
|
369
|
+
this._resumePermissionActivity();
|
|
370
|
+
this.updateTab();
|
|
371
|
+
const app = getApp();
|
|
372
|
+
if (app.activeSession === this) {
|
|
373
|
+
app.updateQuestionIndicator();
|
|
374
|
+
}
|
|
375
|
+
app.sessionManager.saveSessions();
|
|
376
|
+
return true;
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Server ack for a permission_response. ok=false → the request had
|
|
381
|
+
* expired (process restarted) — mark the card so the user knows.
|
|
382
|
+
*/
|
|
383
|
+
handlePermissionResolved(frame) {
|
|
384
|
+
if (frame.ok) return;
|
|
385
|
+
const msg = this.messages.find(m => m.role === 'permission' && m.requestId === frame.request_id);
|
|
386
|
+
if (msg) {
|
|
387
|
+
msg.decision = 'expired';
|
|
388
|
+
msg.answered = true;
|
|
389
|
+
msg.waitedMs = this._permWaitedMs(msg);
|
|
390
|
+
this._rerenderPermissionCard(msg);
|
|
391
|
+
this._resumePermissionActivity();
|
|
392
|
+
getApp().sessionManager.saveSessions();
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Find the permission card for a denial the USER made by hand (clicked
|
|
398
|
+
* Deny on the card). Used to tell manual denies apart from permission-mode
|
|
399
|
+
* auto-denies: the mode-explainer error card ("blocked by X mode — change
|
|
400
|
+
* the permission level…") is wrong and redundant for a manual decision —
|
|
401
|
+
* the answered card and the tool error (which quotes the user's feedback)
|
|
402
|
+
* already tell the story. Matches by tool_use_id when both sides have it,
|
|
403
|
+
* else falls back to tool name.
|
|
404
|
+
* @param {string} toolName
|
|
405
|
+
* @param {string|null} toolUseId
|
|
406
|
+
* @returns {Object|undefined} the denied card message, if any
|
|
407
|
+
*/
|
|
408
|
+
findManualDenyCard(toolName, toolUseId) {
|
|
409
|
+
return [...this.messages].reverse().find(m =>
|
|
410
|
+
m.role === 'permission' && m.decision === 'deny' &&
|
|
411
|
+
((toolUseId && m.toolUseId) ? m.toolUseId === toolUseId : m.toolName === toolName));
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* How long a permission ask waited before it was resolved/expired, anchored
|
|
416
|
+
* on the card's server timestamp (same anchor as the live elapsed ticker in
|
|
417
|
+
* chat-controller). Clamped to >= 0 to absorb minor client/server skew.
|
|
418
|
+
*/
|
|
419
|
+
_permWaitedMs(msg) {
|
|
420
|
+
const start = Date.parse(msg.timestamp);
|
|
421
|
+
return Math.max(0, Date.now() - (Number.isNaN(start) ? Date.now() : start));
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Expire every unanswered permission card — called on session_ended: the
|
|
426
|
+
* provider process died and the server dropped its pending asks (a
|
|
427
|
+
* response can only reach the stdin that asked), so these cards can never
|
|
428
|
+
* be answered. Clears the paused-activity bookkeeping without touching the
|
|
429
|
+
* activity strip itself; the session_ended handler sets the final state.
|
|
430
|
+
*/
|
|
431
|
+
expirePendingPermissionCards() {
|
|
432
|
+
const pending = this.messages.filter(m => m.role === 'permission' && !m.answered);
|
|
433
|
+
if (!pending.length) return;
|
|
434
|
+
for (const msg of pending) {
|
|
435
|
+
msg.answered = true;
|
|
436
|
+
msg.decision = 'expired';
|
|
437
|
+
msg.waitedMs = this._permWaitedMs(msg);
|
|
438
|
+
this._rerenderPermissionCard(msg);
|
|
439
|
+
}
|
|
440
|
+
this._permPausedAt = null;
|
|
441
|
+
this._activityBeforePermission = null;
|
|
442
|
+
this.pendingQuestionId = null;
|
|
443
|
+
this.updateTab();
|
|
444
|
+
const app = getApp();
|
|
445
|
+
if (app.activeSession === this) app.updateQuestionIndicator();
|
|
446
|
+
app.sessionManager.saveSessions();
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Un-pause the activity strip after the last pending ask is answered:
|
|
451
|
+
* shift the turn start time forward by the paused span (so the elapsed
|
|
452
|
+
* timer resumes where it froze) and restore the pre-ask activity.
|
|
453
|
+
*/
|
|
454
|
+
_resumePermissionActivity() {
|
|
455
|
+
if (!this._permPausedAt) return;
|
|
456
|
+
// Parallel tool calls can stack asks — stay paused until all answered
|
|
457
|
+
if (this.messages.some(m => m.role === 'permission' && !m.answered)) return;
|
|
458
|
+
const pausedMs = Date.now() - this._permPausedAt;
|
|
459
|
+
this._permPausedAt = null;
|
|
460
|
+
if (this._turnStartTime) this._turnStartTime += pausedMs;
|
|
461
|
+
const prev = this._activityBeforePermission;
|
|
462
|
+
this._activityBeforePermission = null;
|
|
463
|
+
this._setActivity(prev
|
|
464
|
+
? { ...prev, paused: false }
|
|
465
|
+
: { active: true, icon: 'sparkle', label: S.activity.states.working });
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
/** Re-render a permission card in place (if this session is visible). */
|
|
469
|
+
_rerenderPermissionCard(msg) {
|
|
470
|
+
const app = getApp();
|
|
471
|
+
if (app.activeSession !== this) return;
|
|
472
|
+
const el = document.getElementById(`msg-${msg.id}`);
|
|
473
|
+
if (el) {
|
|
474
|
+
el.replaceWith(app.chatCtrl.createMessageElement(msg));
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Submit answers for AskUserQuestion tool
|
|
480
|
+
* @param {string} toolId - The tool_use_id
|
|
481
|
+
* @param {Object} answers - Answers keyed by question header
|
|
482
|
+
*/
|
|
483
|
+
sendToolAnswer(toolId, answers, comment = '') {
|
|
484
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
485
|
+
console.error('Cannot send tool answer: WebSocket not connected');
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Find the question message to get original questions for context
|
|
490
|
+
const questionMsg = this.messages.find(m => m.role === 'question' && m.toolId === toolId);
|
|
491
|
+
const questions = questionMsg?.questions || [];
|
|
492
|
+
|
|
493
|
+
const message = {
|
|
494
|
+
type: 'tool_answer',
|
|
495
|
+
tool_use_id: toolId,
|
|
496
|
+
answers: answers,
|
|
497
|
+
questions: questions // Include original questions for server to format
|
|
498
|
+
};
|
|
499
|
+
if (comment) message.comment = comment;
|
|
500
|
+
|
|
501
|
+
this.ws.send(JSON.stringify(message));
|
|
502
|
+
debug.log('Sent tool answer:', toolId, answers);
|
|
503
|
+
|
|
504
|
+
// Update the question message as answered (reuse questionMsg from above)
|
|
505
|
+
if (questionMsg) {
|
|
506
|
+
questionMsg.answered = true;
|
|
507
|
+
questionMsg._editing = false; // leave edit mode on (re)send
|
|
508
|
+
questionMsg.answers = answers;
|
|
509
|
+
questionMsg.comment = comment;
|
|
510
|
+
const app = getApp();
|
|
511
|
+
if (app.activeSession === this) {
|
|
512
|
+
app.updateQuestionMessage(questionMsg);
|
|
513
|
+
}
|
|
514
|
+
app.sessionManager.saveSessions();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.pendingQuestionId = null;
|
|
518
|
+
this.updateTab(); // Remove ? badge now that question is answered
|
|
519
|
+
// Clear the floating question indicator
|
|
520
|
+
const app = getApp();
|
|
521
|
+
if (app.activeSession === this) {
|
|
522
|
+
app.updateQuestionIndicator();
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Submit answers for grouped AskUserQuestion (wizard mode)
|
|
529
|
+
* Sends answers for each tool entry sequentially
|
|
530
|
+
* @param {Array} entries - Array of {toolId, questions, answers}
|
|
531
|
+
* @param {Object} questionMsg - The question message object
|
|
532
|
+
*/
|
|
533
|
+
sendToolAnswers(entries, questionMsg, comment = '') {
|
|
534
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
535
|
+
console.error('Cannot send tool answers: WebSocket not connected');
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Send answer for each entry. The free-text comment is shared across the
|
|
540
|
+
// whole card, so it rides on the LAST frame only (each frame becomes its
|
|
541
|
+
// own follow-up message server-side — attaching it to every entry would
|
|
542
|
+
// repeat the comment N times).
|
|
543
|
+
entries.forEach((entry, i) => {
|
|
544
|
+
const message = {
|
|
545
|
+
type: 'tool_answer',
|
|
546
|
+
tool_use_id: entry.toolId,
|
|
547
|
+
answers: entry.answers,
|
|
548
|
+
questions: entry.questions // Include original questions for server to format
|
|
549
|
+
};
|
|
550
|
+
if (comment && i === entries.length - 1) message.comment = comment;
|
|
551
|
+
this.ws.send(JSON.stringify(message));
|
|
552
|
+
debug.log('Sent tool answer:', entry.toolId, entry.answers);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Update the question message as answered
|
|
556
|
+
if (questionMsg) {
|
|
557
|
+
questionMsg.answered = true;
|
|
558
|
+
questionMsg._editing = false; // leave edit mode on (re)send
|
|
559
|
+
questionMsg.comment = comment;
|
|
560
|
+
// Update answers in entries
|
|
561
|
+
if (questionMsg.entries) {
|
|
562
|
+
questionMsg.entries.forEach((e, i) => {
|
|
563
|
+
if (entries[i]) {
|
|
564
|
+
e.answers = entries[i].answers;
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// Also update legacy answers field for backward compatibility
|
|
569
|
+
questionMsg.answers = entries[0]?.answers || {};
|
|
570
|
+
|
|
571
|
+
const app = getApp();
|
|
572
|
+
if (app.activeSession === this) {
|
|
573
|
+
app.updateQuestionMessage(questionMsg);
|
|
574
|
+
}
|
|
575
|
+
app.sessionManager.saveSessions();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.pendingQuestionId = null;
|
|
579
|
+
this.updateTab(); // Remove ? badge now that question is answered
|
|
580
|
+
// Clear the floating question indicator
|
|
581
|
+
const appRef = getApp();
|
|
582
|
+
if (appRef.activeSession === this) {
|
|
583
|
+
appRef.updateQuestionIndicator();
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
},
|
|
587
|
+
};
|