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,915 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThinkingController - Handles rendering of Claude's thinking blocks
|
|
3
|
+
*
|
|
4
|
+
* DESIGN (2025-12-29):
|
|
5
|
+
* - Each thinking step shown inline with its tools
|
|
6
|
+
* - Reasoning text is collapsed (one-line preview), expandable on click
|
|
7
|
+
* - Tools are ALWAYS visible right after their reasoning step
|
|
8
|
+
* - Preserves chronological order: thought1 + tools1, thought2 + tools2, etc.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { $, escapeHtml, escapeAttr, parseTaskUsage, formatTokensBadge, formatDuration } from '../utils.js';
|
|
12
|
+
import { getToolCollapseMode, getToolCategory } from '../widgets/config-widget.js';
|
|
13
|
+
|
|
14
|
+
export class ThinkingController {
|
|
15
|
+
constructor(ctx) {
|
|
16
|
+
this.ctx = ctx;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════
|
|
20
|
+
// THINKING MESSAGE RENDERING
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a thinking message with automatic grouping
|
|
25
|
+
* Each thinking message becomes a step with its tools
|
|
26
|
+
*/
|
|
27
|
+
renderThinkingMessageWithGrouping(msg) {
|
|
28
|
+
const messages = this.ctx.getMessageContainer();
|
|
29
|
+
const lastChild = messages.lastElementChild;
|
|
30
|
+
|
|
31
|
+
// If last element is a thinking section from the SAME turn, add to it
|
|
32
|
+
// (never group across turn boundaries - see commit 06a62c8 for precedent)
|
|
33
|
+
if (lastChild?.classList.contains('thinking') &&
|
|
34
|
+
lastChild.querySelector('.thinking-section') &&
|
|
35
|
+
lastChild.dataset.turnId == msg.turnId) {
|
|
36
|
+
this.addStepToSection(lastChild, msg);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Otherwise create new thinking section
|
|
41
|
+
const div = document.createElement('div');
|
|
42
|
+
div.className = 'message thinking';
|
|
43
|
+
div.id = `msg-${msg.id}`;
|
|
44
|
+
div.dataset.turnId = msg.turnId;
|
|
45
|
+
div.innerHTML = this._renderSection([msg]);
|
|
46
|
+
messages.appendChild(div);
|
|
47
|
+
|
|
48
|
+
// Check for gutter space after DOM is updated
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
const section = div.querySelector('.thinking-section');
|
|
51
|
+
if (section) this.checkGutterSpace(section);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add a new step to an existing thinking section
|
|
57
|
+
*/
|
|
58
|
+
addStepToSection(sectionElement, msg) {
|
|
59
|
+
const section = sectionElement.querySelector('.thinking-section');
|
|
60
|
+
if (!section) return;
|
|
61
|
+
|
|
62
|
+
// Prevent duplicate steps
|
|
63
|
+
if (section.querySelector(`.thinking-step[data-msg-id="${msg.id}"]`)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const existingSteps = section.querySelectorAll('.thinking-step');
|
|
68
|
+
const stepNum = existingSteps.length + 1;
|
|
69
|
+
const sectionId = section.id;
|
|
70
|
+
|
|
71
|
+
const stepHtml = this._renderStep(msg, stepNum, sectionId);
|
|
72
|
+
section.insertAdjacentHTML('beforeend', stepHtml);
|
|
73
|
+
|
|
74
|
+
// Check gutter space (may not have been checked if this is first tool)
|
|
75
|
+
requestAnimationFrame(() => this.checkGutterSpace(section));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a complete thinking section
|
|
80
|
+
*/
|
|
81
|
+
_renderSection(thinkingMsgs) {
|
|
82
|
+
const sectionId = `thinking-section-${thinkingMsgs[0].id}`;
|
|
83
|
+
|
|
84
|
+
const stepsHtml = thinkingMsgs.map((msg, idx) => {
|
|
85
|
+
return this._renderStep(msg, idx + 1, sectionId);
|
|
86
|
+
}).join('');
|
|
87
|
+
|
|
88
|
+
// Only show collapse button if there are any tools
|
|
89
|
+
// Button at START of section so sticky-top works when scrolling down
|
|
90
|
+
// Double chevrons indicate "collapse ALL" vs single chevron for individual tools
|
|
91
|
+
const hasTools = thinkingMsgs.some(msg => msg.tools && msg.tools.length > 0);
|
|
92
|
+
const collapseBtn = hasTools ? `
|
|
93
|
+
<button class="thinking-collapse-tools-btn" data-act="toggle-thinking-tools" data-id="${escapeAttr(sectionId)}" data-tooltip="Collapse/expand all tool outputs">
|
|
94
|
+
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
|
95
|
+
<path d="M7 13l5 5 5-5M7 6l5 5 5-5"/>
|
|
96
|
+
</svg>
|
|
97
|
+
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
|
98
|
+
<path d="M13 7l5 5-5 5M6 7l5 5-5 5"/>
|
|
99
|
+
</svg>
|
|
100
|
+
</button>
|
|
101
|
+
` : '';
|
|
102
|
+
|
|
103
|
+
return `<div class="thinking-section" id="${sectionId}">${collapseBtn}${stepsHtml}</div>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render a single step: collapsed reasoning + visible tools
|
|
108
|
+
* Text is truncated by default, shows full markdown when expanded
|
|
109
|
+
*/
|
|
110
|
+
_renderStep(msg, stepNum, sectionId) {
|
|
111
|
+
const content = (msg.content || '').trim();
|
|
112
|
+
const hasContent = content.length > 0;
|
|
113
|
+
|
|
114
|
+
// Render tools for this step (always visible)
|
|
115
|
+
const toolsHtml = this._renderStepTools(msg.tools || []);
|
|
116
|
+
|
|
117
|
+
// Preview text (plain, truncated, markdown stripped) - shown when collapsed
|
|
118
|
+
const strippedContent = this._stripMarkdownSyntax(content);
|
|
119
|
+
const previewText = strippedContent.slice(0, 200);
|
|
120
|
+
const previewHtml = hasContent
|
|
121
|
+
? `<span class="thinking-step-preview">${escapeHtml(previewText)}${strippedContent.length > 200 ? '...' : ''}</span>`
|
|
122
|
+
: '';
|
|
123
|
+
|
|
124
|
+
// Full content with markdown - shown when expanded
|
|
125
|
+
const fullContentHtml = hasContent
|
|
126
|
+
? `<div class="thinking-step-content">${this._renderMarkdown(content)}</div>`
|
|
127
|
+
: '';
|
|
128
|
+
|
|
129
|
+
// The API hands us signature-only thinking blocks (thinking: "") often
|
|
130
|
+
// enough that a permanently-empty clickable bar is a real artifact, so
|
|
131
|
+
// drop the header when there is nothing to preview or expand. The step
|
|
132
|
+
// itself always stays — it is the DOM anchor updateThinkingMessageTool()
|
|
133
|
+
// looks up by data-msg-id when tools stream in afterwards. CSS keeps a
|
|
134
|
+
// header-less, tool-less step hidden until a tool card actually lands.
|
|
135
|
+
const headerHtml = hasContent
|
|
136
|
+
? `<div class="thinking-step-header" data-act="toggle-thinking-step" data-id="${escapeAttr(sectionId)}" data-step="${stepNum}">
|
|
137
|
+
${previewHtml}
|
|
138
|
+
<span class="thinking-step-expand">›</span>
|
|
139
|
+
</div>`
|
|
140
|
+
: '';
|
|
141
|
+
|
|
142
|
+
return `
|
|
143
|
+
<div class="thinking-step" data-step="${stepNum}" data-msg-id="${msg.id}">
|
|
144
|
+
${headerHtml}
|
|
145
|
+
${fullContentHtml}
|
|
146
|
+
<div class="thinking-step-tools">
|
|
147
|
+
${toolsHtml}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render markdown content using the app's markdown renderer
|
|
155
|
+
*/
|
|
156
|
+
_renderMarkdown(content) {
|
|
157
|
+
if (!content) return '';
|
|
158
|
+
if (this.ctx.markdown) {
|
|
159
|
+
return this.ctx.markdown.render(content);
|
|
160
|
+
}
|
|
161
|
+
// Fallback: escape HTML and preserve whitespace
|
|
162
|
+
return `<pre style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Strip markdown syntax for clean preview text
|
|
167
|
+
* Safe approach: just removes syntax markers, doesn't render
|
|
168
|
+
*/
|
|
169
|
+
_stripMarkdownSyntax(text) {
|
|
170
|
+
if (!text) return '';
|
|
171
|
+
return text
|
|
172
|
+
// Remove heading markers (# ## ### etc)
|
|
173
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
174
|
+
// Remove bold/italic markers
|
|
175
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
176
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
177
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
178
|
+
// Only match _italic_ at word boundaries (not mid-word like ai_overview)
|
|
179
|
+
.replace(/(^|\s)_([^\s_]+)_(\s|$)/g, '$1$2$3')
|
|
180
|
+
// Remove inline code backticks
|
|
181
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
182
|
+
// Remove blockquote markers
|
|
183
|
+
.replace(/^>\s*/gm, '')
|
|
184
|
+
// Remove list markers
|
|
185
|
+
.replace(/^[-*+]\s+/gm, '')
|
|
186
|
+
.replace(/^\d+\.\s+/gm, '')
|
|
187
|
+
// Remove link syntax [text](url) -> text
|
|
188
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
189
|
+
// Collapse multiple spaces/newlines into single space
|
|
190
|
+
.replace(/\s+/g, ' ')
|
|
191
|
+
.trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Render tools for a step (always visible)
|
|
196
|
+
* Groups sub-agent tools under their parent Task tool
|
|
197
|
+
*/
|
|
198
|
+
_renderStepTools(tools) {
|
|
199
|
+
if (!tools || tools.length === 0) return '';
|
|
200
|
+
|
|
201
|
+
// Group tools: Tasks capture subsequent tools until completed
|
|
202
|
+
const groups = this._groupToolsByTask(tools);
|
|
203
|
+
|
|
204
|
+
return groups.map(group => {
|
|
205
|
+
if (group.type === 'task') {
|
|
206
|
+
return this._renderTaskGroup(group);
|
|
207
|
+
} else {
|
|
208
|
+
return this._renderSingleTool(group.tool);
|
|
209
|
+
}
|
|
210
|
+
}).filter(Boolean).join('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Group tools by Task ownership
|
|
215
|
+
* Task tools capture all subsequent tools until we hit another Task tool
|
|
216
|
+
* (regardless of completion status - completion only affects the badge)
|
|
217
|
+
*/
|
|
218
|
+
_groupToolsByTask(tools) {
|
|
219
|
+
const groups = [];
|
|
220
|
+
let currentTaskGroup = null;
|
|
221
|
+
|
|
222
|
+
for (const tool of tools) {
|
|
223
|
+
if (tool.toolName === 'Task') {
|
|
224
|
+
// Close any previous task group
|
|
225
|
+
if (currentTaskGroup) {
|
|
226
|
+
groups.push(currentTaskGroup);
|
|
227
|
+
}
|
|
228
|
+
// Start new task group
|
|
229
|
+
currentTaskGroup = {
|
|
230
|
+
type: 'task',
|
|
231
|
+
task: tool,
|
|
232
|
+
children: []
|
|
233
|
+
};
|
|
234
|
+
} else if (currentTaskGroup) {
|
|
235
|
+
// Add to current task group (all tools after Task belong to it)
|
|
236
|
+
currentTaskGroup.children.push(tool);
|
|
237
|
+
} else {
|
|
238
|
+
// Independent tool (before any Task)
|
|
239
|
+
groups.push({ type: 'tool', tool });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Don't forget the last group
|
|
244
|
+
if (currentTaskGroup) {
|
|
245
|
+
groups.push(currentTaskGroup);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return groups;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Render a single tool (non-Task)
|
|
253
|
+
*/
|
|
254
|
+
_renderSingleTool(tool) {
|
|
255
|
+
const toolType = getToolCategory(tool.toolName);
|
|
256
|
+
const mode = getToolCollapseMode('thinking', toolType);
|
|
257
|
+
const expandOpts = { defaultExpanded: mode === 'expanded' };
|
|
258
|
+
const msg = {
|
|
259
|
+
id: tool.toolId,
|
|
260
|
+
toolInput: tool.toolInput,
|
|
261
|
+
toolOutput: tool.toolOutput,
|
|
262
|
+
toolError: tool.toolError,
|
|
263
|
+
toolCompleted: tool.toolCompleted || !!tool.toolOutput
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
let html = '';
|
|
267
|
+
// Read/Write/Edit need file_path. If missing (truncated input, partial
|
|
268
|
+
// streaming state, malformed log), fall through to the compact renderer
|
|
269
|
+
// — its renderThinkingToolCompact path already handles missing inputs.
|
|
270
|
+
const hasFilePath = !!tool.toolInput?.file_path;
|
|
271
|
+
switch (tool.toolName) {
|
|
272
|
+
case 'Edit':
|
|
273
|
+
html = hasFilePath
|
|
274
|
+
? this.ctx.toolRenderer.renderEditDiff(tool.toolInput, tool.toolOutput, tool.toolId, tool.startLine)
|
|
275
|
+
: this.ctx.toolRenderer.renderThinkingToolCompact(tool);
|
|
276
|
+
break;
|
|
277
|
+
case 'Write':
|
|
278
|
+
html = hasFilePath
|
|
279
|
+
? this.ctx.toolRenderer.renderWriteBlock(msg, expandOpts)
|
|
280
|
+
: this.ctx.toolRenderer.renderThinkingToolCompact(tool);
|
|
281
|
+
break;
|
|
282
|
+
case 'Read':
|
|
283
|
+
html = hasFilePath
|
|
284
|
+
? this.ctx.toolRenderer.renderReadBlock(msg, expandOpts)
|
|
285
|
+
: this.ctx.toolRenderer.renderThinkingToolCompact(tool);
|
|
286
|
+
break;
|
|
287
|
+
case 'Grep':
|
|
288
|
+
html = this.ctx.toolRenderer.renderGrepBlock(msg, expandOpts);
|
|
289
|
+
break;
|
|
290
|
+
case 'Glob':
|
|
291
|
+
html = this.ctx.toolRenderer.renderGlobBlock(msg, expandOpts);
|
|
292
|
+
break;
|
|
293
|
+
case 'Bash':
|
|
294
|
+
case 'Shell':
|
|
295
|
+
html = this.ctx.toolRenderer.renderBashBlock(msg, expandOpts);
|
|
296
|
+
break;
|
|
297
|
+
case 'TodoWrite':
|
|
298
|
+
msg.toolName = 'TodoWrite';
|
|
299
|
+
html = this.ctx.toolRenderer.renderTodoBlock(msg);
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
html = this.ctx.toolRenderer.renderThinkingToolCompact(tool);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!html) return '';
|
|
306
|
+
|
|
307
|
+
// Gutter icon for per-tool collapse (positioned in right margin via CSS)
|
|
308
|
+
const gutterIcon = `
|
|
309
|
+
<button class="tool-gutter-icon" data-act="toggle-tool-collapse" data-id="${escapeAttr(tool.toolId)}" data-tooltip="Collapse/expand this tool">
|
|
310
|
+
<svg class="gutter-collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
311
|
+
<path d="M19 9l-7 7-7-7"/>
|
|
312
|
+
</svg>
|
|
313
|
+
<svg class="gutter-expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
314
|
+
<path d="M9 5l7 7-7 7"/>
|
|
315
|
+
</svg>
|
|
316
|
+
</button>
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const collapsedClass = mode === 'collapsed' ? ' tool-collapsed' : '';
|
|
320
|
+
return `<div class="thinking-tool-card${collapsedClass}" data-tool-id="${tool.toolId}">${gutterIcon}${html}</div>`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Render a Task group with collapsible children
|
|
325
|
+
*/
|
|
326
|
+
_renderTaskGroup(group) {
|
|
327
|
+
const { task, children } = group;
|
|
328
|
+
const isComplete = task.toolCompleted || !!task.toolOutput;
|
|
329
|
+
const desc = task.toolInput?.description || 'Running task...';
|
|
330
|
+
const subagentType = task.toolInput?.subagent_type || '';
|
|
331
|
+
|
|
332
|
+
// Status badge
|
|
333
|
+
const statusBadge = isComplete
|
|
334
|
+
? '<span class="task-group-badge task-group-badge-success">Done</span>'
|
|
335
|
+
: '<span class="task-group-badge task-group-badge-pending">Running...</span>';
|
|
336
|
+
|
|
337
|
+
// Count badge for children
|
|
338
|
+
const childCount = children.length;
|
|
339
|
+
const countBadge = childCount > 0
|
|
340
|
+
? `<span class="task-group-count">${childCount} tool${childCount !== 1 ? 's' : ''}</span>`
|
|
341
|
+
: '';
|
|
342
|
+
|
|
343
|
+
// Render children (collapsed by default)
|
|
344
|
+
const childrenHtml = children.length > 0
|
|
345
|
+
? children.map(child => this._renderSingleTool(child)).join('')
|
|
346
|
+
: '';
|
|
347
|
+
|
|
348
|
+
// Parse agent response from toolOutput
|
|
349
|
+
const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(task.toolOutput);
|
|
350
|
+
|
|
351
|
+
// Usage badges (tokens + duration)
|
|
352
|
+
let usageBadges = '';
|
|
353
|
+
if (usage) {
|
|
354
|
+
const tokBadge = formatTokensBadge(usage.totalTokens);
|
|
355
|
+
const durBadge = formatDuration(usage.durationMs);
|
|
356
|
+
if (tokBadge) usageBadges += `<span class="task-group-tokens">${tokBadge}</span>`;
|
|
357
|
+
if (durBadge) usageBadges += `<span class="task-group-duration">${durBadge}</span>`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Preview for collapsed state (first ~150 chars, markdown stripped)
|
|
361
|
+
let outputPreview = '';
|
|
362
|
+
if (isComplete && agentResponse) {
|
|
363
|
+
const strippedResponse = this._stripMarkdownSyntax(agentResponse);
|
|
364
|
+
const preview = strippedResponse.slice(0, 150).trim();
|
|
365
|
+
if (preview) {
|
|
366
|
+
outputPreview = `<div class="task-group-preview">${escapeHtml(preview)}${strippedResponse.length > 150 ? '...' : ''}</div>`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Full response for expanded state (rendered with markdown)
|
|
371
|
+
let fullResponse = '';
|
|
372
|
+
if (isComplete && agentResponse) {
|
|
373
|
+
fullResponse = `<div class="task-group-response">${this._renderMarkdown(agentResponse)}</div>`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Show expand chevron if there are children OR a response to show
|
|
377
|
+
const hasExpandableContent = childCount > 0 || agentResponse;
|
|
378
|
+
|
|
379
|
+
return `
|
|
380
|
+
<div class="task-group" data-task-id="${escapeAttr(task.toolId)}" data-complete="${isComplete}">
|
|
381
|
+
<div class="task-group-header" data-act="toggle-task-group" data-id="${escapeAttr(task.toolId)}">
|
|
382
|
+
<span class="task-group-icon">
|
|
383
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
384
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
385
|
+
<circle cx="9" cy="7" r="4"/>
|
|
386
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
387
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
388
|
+
</svg>
|
|
389
|
+
</span>
|
|
390
|
+
<span class="task-group-label">Task</span>
|
|
391
|
+
<span class="task-group-desc">${escapeHtml(desc)}</span>
|
|
392
|
+
${subagentType ? `<span class="task-group-type">${escapeHtml(subagentType)}</span>` : ''}
|
|
393
|
+
${countBadge}
|
|
394
|
+
${usageBadges}
|
|
395
|
+
${statusBadge}
|
|
396
|
+
${hasExpandableContent ? '<span class="task-group-expand">›</span>' : ''}
|
|
397
|
+
</div>
|
|
398
|
+
${outputPreview}
|
|
399
|
+
<div class="task-group-body">
|
|
400
|
+
${fullResponse}
|
|
401
|
+
${childrenHtml ? `<div class="task-group-children">${childrenHtml}</div>` : ''}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Parse Task toolOutput to extract text content
|
|
409
|
+
* Output format is usually: [{"type":"text","text":"..."}]
|
|
410
|
+
*/
|
|
411
|
+
_parseTaskOutputWithUsage(toolOutput) {
|
|
412
|
+
const raw = this._parseTaskOutput(toolOutput);
|
|
413
|
+
return parseTaskUsage(raw);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_parseTaskOutput(toolOutput) {
|
|
417
|
+
if (!toolOutput) return '';
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
// If it's a string that looks like JSON array, parse it
|
|
421
|
+
let parsed = toolOutput;
|
|
422
|
+
if (typeof toolOutput === 'string') {
|
|
423
|
+
if (toolOutput.startsWith('[')) {
|
|
424
|
+
parsed = JSON.parse(toolOutput);
|
|
425
|
+
} else {
|
|
426
|
+
// Plain string, return as-is
|
|
427
|
+
return toolOutput;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Extract text from content blocks
|
|
432
|
+
if (Array.isArray(parsed)) {
|
|
433
|
+
return parsed
|
|
434
|
+
.filter(block => block.type === 'text' && block.text)
|
|
435
|
+
.map(block => block.text)
|
|
436
|
+
.join('\n\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Object with text property
|
|
440
|
+
if (parsed && typeof parsed === 'object' && parsed.text) {
|
|
441
|
+
return parsed.text;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fallback: stringify if we couldn't parse
|
|
445
|
+
return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
|
|
446
|
+
} catch (e) {
|
|
447
|
+
// JSON parse failed, return as-is
|
|
448
|
+
return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ═══════════════════════════════════════════════════════════════
|
|
453
|
+
// TOGGLE FUNCTIONS
|
|
454
|
+
// ═══════════════════════════════════════════════════════════════
|
|
455
|
+
|
|
456
|
+
toggleThinkingStep(sectionId, stepNum) {
|
|
457
|
+
const section = $(`#${sectionId}`);
|
|
458
|
+
if (!section) return;
|
|
459
|
+
|
|
460
|
+
const step = section.querySelector(`.thinking-step[data-step="${stepNum}"]`);
|
|
461
|
+
if (step) {
|
|
462
|
+
step.classList.toggle('expanded');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Toggle all tool outputs in a thinking section (collapse/expand)
|
|
468
|
+
* Also syncs individual tool card states so their icons update correctly
|
|
469
|
+
*/
|
|
470
|
+
toggleThinkingTools(sectionId) {
|
|
471
|
+
const section = $(`#${sectionId}`);
|
|
472
|
+
if (!section) return;
|
|
473
|
+
|
|
474
|
+
const isCollapsing = !section.classList.contains('tools-collapsed');
|
|
475
|
+
section.classList.toggle('tools-collapsed');
|
|
476
|
+
|
|
477
|
+
// Sync all individual tool cards to match the section state
|
|
478
|
+
// This ensures per-tool icons show correct expand/collapse state
|
|
479
|
+
const toolCards = section.querySelectorAll('.thinking-tool-card');
|
|
480
|
+
toolCards.forEach(card => {
|
|
481
|
+
if (isCollapsing) {
|
|
482
|
+
card.classList.add('tool-collapsed');
|
|
483
|
+
} else {
|
|
484
|
+
card.classList.remove('tool-collapsed');
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// After collapsing, scroll to bring the section into view
|
|
489
|
+
// But only if it's not already visible (avoid jarring jumps)
|
|
490
|
+
if (isCollapsing) {
|
|
491
|
+
requestAnimationFrame(() => {
|
|
492
|
+
const message = section.closest('.message');
|
|
493
|
+
if (message) {
|
|
494
|
+
const container = document.getElementById('messages-container');
|
|
495
|
+
if (container) {
|
|
496
|
+
const msgRect = message.getBoundingClientRect();
|
|
497
|
+
const containerRect = container.getBoundingClientRect();
|
|
498
|
+
// Only scroll if message top is above visible area
|
|
499
|
+
if (msgRect.top < containerRect.top) {
|
|
500
|
+
message.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Toggle a single tool's collapse state
|
|
510
|
+
*/
|
|
511
|
+
toggleToolCollapse(toolId) {
|
|
512
|
+
const toolCard = document.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
|
|
513
|
+
if (toolCard) {
|
|
514
|
+
toolCard.classList.toggle('tool-collapsed');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Check if there's margin space for gutter icons
|
|
520
|
+
* Called after render and on resize
|
|
521
|
+
*/
|
|
522
|
+
checkGutterSpace(section) {
|
|
523
|
+
const messageEl = section.closest('.message');
|
|
524
|
+
if (!messageEl) return;
|
|
525
|
+
|
|
526
|
+
// Use the scrolling container, not the inner messages div
|
|
527
|
+
// #messages-container has padding and may have extra space on the right
|
|
528
|
+
const container = document.getElementById('messages-container');
|
|
529
|
+
if (!container) return;
|
|
530
|
+
|
|
531
|
+
const messageRect = messageEl.getBoundingClientRect();
|
|
532
|
+
const containerRect = container.getBoundingClientRect();
|
|
533
|
+
|
|
534
|
+
// Right space: distance from message edge to container edge
|
|
535
|
+
// Subtract ~20px for scrollbar buffer
|
|
536
|
+
const rightSpace = containerRect.right - messageRect.right - 20;
|
|
537
|
+
|
|
538
|
+
if (rightSpace >= 40) {
|
|
539
|
+
section.classList.add('has-gutter-space');
|
|
540
|
+
} else {
|
|
541
|
+
section.classList.remove('has-gutter-space');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Check gutter space for all visible thinking sections
|
|
547
|
+
* Called on window resize
|
|
548
|
+
*/
|
|
549
|
+
checkAllGutterSpace() {
|
|
550
|
+
document.querySelectorAll('.thinking-section').forEach(section => {
|
|
551
|
+
this.checkGutterSpace(section);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Legacy compatibility
|
|
556
|
+
toggleThinking(id) {
|
|
557
|
+
const step = document.querySelector(`.thinking-step[data-msg-id="${id}"]`);
|
|
558
|
+
if (step) step.classList.toggle('expanded');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
toggleThinkingGroup(groupId) {
|
|
562
|
+
// No-op - groups are always visible now
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
toggleThinkingThoughts(sectionId) {
|
|
566
|
+
// No-op - thoughts are always visible now
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
toggleThought(thoughtId) {
|
|
570
|
+
const step = document.querySelector(`.thinking-step[data-msg-id="${thoughtId}"]`);
|
|
571
|
+
if (step) step.classList.toggle('expanded');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════
|
|
575
|
+
// LIVE UPDATES (tool results streaming in)
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Add a tool to a thinking message (live update)
|
|
580
|
+
* @param {Object} msg - The thinking message
|
|
581
|
+
* @param {Object} block - The tool_use block
|
|
582
|
+
* @param {string|null} parentTaskId - Server-provided parent Task ID for nesting
|
|
583
|
+
*/
|
|
584
|
+
updateThinkingMessageTool(msg, block, parentTaskId = null) {
|
|
585
|
+
const step = document.querySelector(`.thinking-step[data-msg-id="${msg.id}"]`);
|
|
586
|
+
if (!step) return;
|
|
587
|
+
|
|
588
|
+
const toolsContainer = step.querySelector('.thinking-step-tools');
|
|
589
|
+
if (!toolsContainer) return;
|
|
590
|
+
|
|
591
|
+
// Check if tool already exists
|
|
592
|
+
if (toolsContainer.querySelector(`[data-tool-id="${block.id}"]`)) return;
|
|
593
|
+
|
|
594
|
+
const tool = {
|
|
595
|
+
toolName: block.name,
|
|
596
|
+
toolInput: block.input,
|
|
597
|
+
toolId: block.id,
|
|
598
|
+
toolCompleted: false,
|
|
599
|
+
toolOutput: null,
|
|
600
|
+
parentTaskId
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
if (block.name === 'Task') {
|
|
604
|
+
// Render new Task group
|
|
605
|
+
const taskHtml = this._renderTaskGroup({
|
|
606
|
+
task: tool,
|
|
607
|
+
children: []
|
|
608
|
+
});
|
|
609
|
+
toolsContainer.insertAdjacentHTML('beforeend', taskHtml);
|
|
610
|
+
} else if (parentTaskId) {
|
|
611
|
+
// Server told us the parent - nest inside that Task group
|
|
612
|
+
this._nestToolInTaskGroup(toolsContainer, parentTaskId, tool);
|
|
613
|
+
} else {
|
|
614
|
+
// No parent info - check for single active Task (fallback heuristic)
|
|
615
|
+
const incompleteTasks = toolsContainer.querySelectorAll(
|
|
616
|
+
'.task-group:not([data-complete="true"])'
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
if (incompleteTasks.length === 1) {
|
|
620
|
+
// Single active Task - safe to nest
|
|
621
|
+
this._nestToolInTaskGroup(
|
|
622
|
+
toolsContainer,
|
|
623
|
+
incompleteTasks[0].dataset.taskId,
|
|
624
|
+
tool
|
|
625
|
+
);
|
|
626
|
+
} else {
|
|
627
|
+
// Multiple or no Tasks - render flat (safe fallback)
|
|
628
|
+
const toolHtml = this._renderSingleTool(tool);
|
|
629
|
+
if (toolHtml) {
|
|
630
|
+
toolsContainer.insertAdjacentHTML('beforeend', toolHtml);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Ensure collapse button exists now that we have a tool
|
|
636
|
+
this._ensureCollapseButton(step);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Ensure the collapse button exists in the thinking section
|
|
641
|
+
* Called when tools are added live (after initial render)
|
|
642
|
+
*/
|
|
643
|
+
_ensureCollapseButton(step) {
|
|
644
|
+
const section = step.closest('.thinking-section');
|
|
645
|
+
if (!section) return;
|
|
646
|
+
|
|
647
|
+
// Check for gutter space (may have tools now)
|
|
648
|
+
this.checkGutterSpace(section);
|
|
649
|
+
|
|
650
|
+
// Already has button
|
|
651
|
+
if (section.querySelector('.thinking-collapse-tools-btn')) return;
|
|
652
|
+
|
|
653
|
+
// Insert at START for sticky-top to work when scrolling down
|
|
654
|
+
// Double chevrons indicate "collapse ALL"
|
|
655
|
+
const sectionId = section.id;
|
|
656
|
+
const btnHtml = `
|
|
657
|
+
<button class="thinking-collapse-tools-btn" data-act="toggle-thinking-tools" data-id="${escapeAttr(sectionId)}" data-tooltip="Collapse/expand all tool outputs">
|
|
658
|
+
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
|
659
|
+
<path d="M7 13l5 5 5-5M7 6l5 5 5-5"/>
|
|
660
|
+
</svg>
|
|
661
|
+
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
|
|
662
|
+
<path d="M13 7l5 5-5 5M6 7l5 5-5 5"/>
|
|
663
|
+
</svg>
|
|
664
|
+
</button>
|
|
665
|
+
`;
|
|
666
|
+
section.insertAdjacentHTML('afterbegin', btnHtml);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Nest a tool inside a Task group
|
|
671
|
+
*/
|
|
672
|
+
_nestToolInTaskGroup(container, taskId, tool) {
|
|
673
|
+
const taskGroup = container.querySelector(`.task-group[data-task-id="${taskId}"]`);
|
|
674
|
+
if (!taskGroup) {
|
|
675
|
+
// Task group not found - render flat as fallback
|
|
676
|
+
console.warn(`[ThinkingController] Task group not found for ${taskId}, rendering flat`);
|
|
677
|
+
const toolHtml = this._renderSingleTool(tool);
|
|
678
|
+
if (toolHtml) {
|
|
679
|
+
container.insertAdjacentHTML('beforeend', toolHtml);
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Get or create children container
|
|
685
|
+
let childrenContainer = taskGroup.querySelector('.task-group-children');
|
|
686
|
+
if (!childrenContainer) {
|
|
687
|
+
const body = taskGroup.querySelector('.task-group-body');
|
|
688
|
+
if (!body) {
|
|
689
|
+
console.error(`[ThinkingController] Task group body not found for ${taskId}`);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
childrenContainer = document.createElement('div');
|
|
693
|
+
childrenContainer.className = 'task-group-children';
|
|
694
|
+
body.appendChild(childrenContainer);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Render and insert tool
|
|
698
|
+
const toolHtml = this._renderSingleTool(tool);
|
|
699
|
+
if (toolHtml) {
|
|
700
|
+
childrenContainer.insertAdjacentHTML('beforeend', toolHtml);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Update count badge
|
|
704
|
+
this._updateTaskChildCount(taskGroup);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Update the child count badge on a Task group
|
|
709
|
+
*/
|
|
710
|
+
_updateTaskChildCount(taskGroup) {
|
|
711
|
+
const children = taskGroup.querySelectorAll('.task-group-children .thinking-tool-card');
|
|
712
|
+
let countBadge = taskGroup.querySelector('.task-group-count');
|
|
713
|
+
const header = taskGroup.querySelector('.task-group-header');
|
|
714
|
+
|
|
715
|
+
const count = children.length;
|
|
716
|
+
|
|
717
|
+
if (count > 0) {
|
|
718
|
+
if (!countBadge) {
|
|
719
|
+
// Create count badge if it doesn't exist
|
|
720
|
+
const statusBadge = header.querySelector('.task-group-badge');
|
|
721
|
+
if (statusBadge) {
|
|
722
|
+
countBadge = document.createElement('span');
|
|
723
|
+
countBadge.className = 'task-group-count';
|
|
724
|
+
statusBadge.before(countBadge);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (countBadge) {
|
|
728
|
+
countBadge.textContent = `${count} tool${count !== 1 ? 's' : ''}`;
|
|
729
|
+
countBadge.style.display = '';
|
|
730
|
+
}
|
|
731
|
+
} else if (countBadge) {
|
|
732
|
+
countBadge.style.display = 'none';
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Update a tool result (live update)
|
|
738
|
+
* @param {Object} msg - The thinking message
|
|
739
|
+
* @param {string} toolId - The tool ID
|
|
740
|
+
* @param {string} content - The tool result content
|
|
741
|
+
* @param {string[]|null} childToolIds - For Task results, list of child tool IDs
|
|
742
|
+
*/
|
|
743
|
+
updateThinkingMessageToolResult(msg, toolId, content, childToolIds = null) {
|
|
744
|
+
// First try standard tool card selector
|
|
745
|
+
let toolCard = document.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
|
|
746
|
+
|
|
747
|
+
if (!toolCard) {
|
|
748
|
+
// Check if it's a Task group
|
|
749
|
+
const taskGroup = document.querySelector(`.task-group[data-task-id="${toolId}"]`);
|
|
750
|
+
if (taskGroup) {
|
|
751
|
+
this._updateTaskGroupResult(taskGroup, msg, toolId, content, childToolIds);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
// Tool not found - this shouldn't happen
|
|
755
|
+
console.warn(`[ThinkingController] Tool card not found for ${toolId}`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Standard tool update
|
|
760
|
+
const tool = msg.tools?.find(t => t.toolId === toolId);
|
|
761
|
+
if (!tool) return;
|
|
762
|
+
|
|
763
|
+
const newHtml = this._renderSingleTool({
|
|
764
|
+
...tool,
|
|
765
|
+
toolOutput: content,
|
|
766
|
+
toolCompleted: true
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
if (newHtml) {
|
|
770
|
+
toolCard.outerHTML = newHtml;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Update a Task group when its result arrives
|
|
776
|
+
*/
|
|
777
|
+
_updateTaskGroupResult(taskGroup, msg, toolId, content, childToolIds) {
|
|
778
|
+
// Mark as complete
|
|
779
|
+
taskGroup.setAttribute('data-complete', 'true');
|
|
780
|
+
|
|
781
|
+
// Update badge
|
|
782
|
+
const badge = taskGroup.querySelector('.task-group-badge');
|
|
783
|
+
if (badge) {
|
|
784
|
+
badge.className = 'task-group-badge task-group-badge-success';
|
|
785
|
+
badge.textContent = 'Done';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Parse agent response and usage data
|
|
789
|
+
const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(content);
|
|
790
|
+
|
|
791
|
+
// Inject usage badges into header
|
|
792
|
+
if (usage) {
|
|
793
|
+
const header = taskGroup.querySelector('.task-group-header');
|
|
794
|
+
if (header) {
|
|
795
|
+
header.querySelectorAll('.task-group-tokens, .task-group-duration').forEach(el => el.remove());
|
|
796
|
+
const statusBadge = taskGroup.querySelector('.task-group-badge');
|
|
797
|
+
const tokText = formatTokensBadge(usage.totalTokens);
|
|
798
|
+
const durText = formatDuration(usage.durationMs);
|
|
799
|
+
const insertBefore = (el) => { if (statusBadge) statusBadge.before(el); else header.appendChild(el); };
|
|
800
|
+
if (durText) {
|
|
801
|
+
const durEl = document.createElement('span');
|
|
802
|
+
durEl.className = 'task-group-duration';
|
|
803
|
+
durEl.textContent = durText;
|
|
804
|
+
insertBefore(durEl);
|
|
805
|
+
}
|
|
806
|
+
if (tokText) {
|
|
807
|
+
const tokEl = document.createElement('span');
|
|
808
|
+
tokEl.className = 'task-group-tokens';
|
|
809
|
+
tokEl.textContent = tokText;
|
|
810
|
+
insertBefore(tokEl);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Update or create preview
|
|
816
|
+
let preview = taskGroup.querySelector('.task-group-preview');
|
|
817
|
+
if (agentResponse) {
|
|
818
|
+
const strippedResponse = this._stripMarkdownSyntax(agentResponse);
|
|
819
|
+
const previewText = strippedResponse.slice(0, 150).trim();
|
|
820
|
+
|
|
821
|
+
if (!preview) {
|
|
822
|
+
const header = taskGroup.querySelector('.task-group-header');
|
|
823
|
+
preview = document.createElement('div');
|
|
824
|
+
preview.className = 'task-group-preview';
|
|
825
|
+
header.after(preview);
|
|
826
|
+
}
|
|
827
|
+
preview.textContent = previewText + (strippedResponse.length > 150 ? '...' : '');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Update or create full response
|
|
831
|
+
let responseContainer = taskGroup.querySelector('.task-group-response');
|
|
832
|
+
const body = taskGroup.querySelector('.task-group-body');
|
|
833
|
+
|
|
834
|
+
if (agentResponse && body) {
|
|
835
|
+
if (!responseContainer) {
|
|
836
|
+
responseContainer = document.createElement('div');
|
|
837
|
+
responseContainer.className = 'task-group-response';
|
|
838
|
+
body.insertBefore(responseContainer, body.firstChild);
|
|
839
|
+
}
|
|
840
|
+
responseContainer.innerHTML = this._renderMarkdown(agentResponse);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Verify/correct child tool grouping if server provided childToolIds
|
|
844
|
+
if (childToolIds && childToolIds.length > 0) {
|
|
845
|
+
this._verifyTaskChildren(taskGroup, childToolIds);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Update count badge
|
|
849
|
+
this._updateTaskChildCount(taskGroup);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Verify and correct child tool grouping based on server-provided IDs
|
|
854
|
+
* This handles cases where tools were rendered flat during parallel execution
|
|
855
|
+
*/
|
|
856
|
+
_verifyTaskChildren(taskGroup, childToolIds) {
|
|
857
|
+
const container = taskGroup.closest('.thinking-step-tools');
|
|
858
|
+
if (!container) return;
|
|
859
|
+
|
|
860
|
+
let childrenContainer = taskGroup.querySelector('.task-group-children');
|
|
861
|
+
if (!childrenContainer) {
|
|
862
|
+
const body = taskGroup.querySelector('.task-group-body');
|
|
863
|
+
if (!body) return;
|
|
864
|
+
childrenContainer = document.createElement('div');
|
|
865
|
+
childrenContainer.className = 'task-group-children';
|
|
866
|
+
body.appendChild(childrenContainer);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Move any misplaced tools into this Task group
|
|
870
|
+
for (const toolId of childToolIds) {
|
|
871
|
+
// Skip if already in this Task group
|
|
872
|
+
if (childrenContainer.querySelector(`[data-tool-id="${toolId}"]`)) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Find the tool anywhere in the container (might be a sibling or in another Task)
|
|
877
|
+
const tool = container.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
|
|
878
|
+
if (tool) {
|
|
879
|
+
// Move it into this Task's children
|
|
880
|
+
childrenContainer.appendChild(tool);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ═══════════════════════════════════════════════════════════════
|
|
886
|
+
// LEGACY METHODS (compatibility)
|
|
887
|
+
// ═══════════════════════════════════════════════════════════════
|
|
888
|
+
|
|
889
|
+
renderThinkingBlock(msg) {
|
|
890
|
+
return this._renderSection([msg]);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
renderThinkingGroup(thinkingMsgs) {
|
|
894
|
+
const div = document.createElement('div');
|
|
895
|
+
div.className = 'message thinking';
|
|
896
|
+
div.id = `thinking-group-${thinkingMsgs[0].id}`;
|
|
897
|
+
div.innerHTML = this._renderSection(thinkingMsgs);
|
|
898
|
+
this.ctx.getMessageContainer().appendChild(div);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
renderThinkingEdits(thinkingMsgs) {
|
|
902
|
+
return ''; // Tools are inline with each step now
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
updateThinkingEditsSection(msg) {
|
|
906
|
+
// No-op - tools update inline
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
createThinkingGroupElement(thinkingGroup) {
|
|
910
|
+
const groupDiv = document.createElement('div');
|
|
911
|
+
groupDiv.className = 'message thinking';
|
|
912
|
+
groupDiv.innerHTML = this._renderSection(thinkingGroup);
|
|
913
|
+
return groupDiv;
|
|
914
|
+
}
|
|
915
|
+
}
|