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,1500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Renderer Module
|
|
3
|
+
* Renders tool blocks (Bash, Read, Edit, Write, etc.) with specialized displays
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import S from './strings.js';
|
|
7
|
+
import { $, escapeHtml, escapeAttr, sanitizeSvg, parseTaskUsage, formatTokensBadge, formatDuration } from './utils.js';
|
|
8
|
+
import { CONFIG } from './config.js';
|
|
9
|
+
import { generateSmartDiff, renderSmartDiff } from './diff-utils.js';
|
|
10
|
+
// URL patterns always needed, path pattern for client-side scanning in colorized output
|
|
11
|
+
import { buildUrlPattern, buildPathPattern, buildStandalonePattern, isValidStandaloneFile, cleanUrlTrailingPunct, parseLineInfo } from './linkify-utils.js';
|
|
12
|
+
import { parseBackgroundTaskOutput, bgTaskTracker } from './background-tasks.js';
|
|
13
|
+
import { getToolCollapseMode, getToolCategory } from './widgets/config-widget.js';
|
|
14
|
+
import { blockMethods } from './tool-renderer-blocks.js';
|
|
15
|
+
import { thinkingMethods } from './tool-renderer-thinking.js';
|
|
16
|
+
import { basename } from './path-utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Clean tool_use_error XML wrappers from error/output text.
|
|
20
|
+
* Claude CLI wraps sibling errors as <tool_use_error>message</tool_use_error>.
|
|
21
|
+
* Returns {text, isSiblingError} — isSiblingError is true when another parallel tool failed.
|
|
22
|
+
*/
|
|
23
|
+
export function cleanToolError(text) {
|
|
24
|
+
if (!text) return { text: '', isSiblingError: false };
|
|
25
|
+
if (typeof text !== 'string') text = (typeof text === 'object' && text !== null) ? JSON.stringify(text, null, 2) : String(text);
|
|
26
|
+
const match = text.match(/<tool_use_error>([\s\S]*?)<\/tool_use_error>/);
|
|
27
|
+
if (match) {
|
|
28
|
+
const inner = match[1].trim();
|
|
29
|
+
const isSiblingError = inner.toLowerCase().includes('sibling tool call errored');
|
|
30
|
+
return { text: inner, isSiblingError };
|
|
31
|
+
}
|
|
32
|
+
return { text, isSiblingError: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ANSI color code to CSS class mapping
|
|
36
|
+
const ANSI_COLORS = {
|
|
37
|
+
'30': 'ansi-black', '31': 'ansi-red', '32': 'ansi-green', '33': 'ansi-yellow',
|
|
38
|
+
'34': 'ansi-blue', '35': 'ansi-magenta', '36': 'ansi-cyan', '37': 'ansi-white',
|
|
39
|
+
'90': 'ansi-bright-black', '91': 'ansi-bright-red', '92': 'ansi-bright-green',
|
|
40
|
+
'93': 'ansi-bright-yellow', '94': 'ansi-bright-blue', '95': 'ansi-bright-magenta',
|
|
41
|
+
'96': 'ansi-bright-cyan', '97': 'ansi-bright-white'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// File extension → highlight.js language mapping (shared across all tool renderers)
|
|
45
|
+
const LANG_MAP = {
|
|
46
|
+
js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
|
|
47
|
+
py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
|
|
48
|
+
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
|
|
49
|
+
php: 'php', swift: 'swift', kt: 'kotlin', scala: 'scala',
|
|
50
|
+
html: 'xml', htm: 'xml', xml: 'xml', svg: 'xml',
|
|
51
|
+
css: 'css', scss: 'scss', less: 'less', sass: 'sass',
|
|
52
|
+
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini', ini: 'ini',
|
|
53
|
+
md: 'markdown', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
|
|
54
|
+
dockerfile: 'dockerfile', makefile: 'makefile',
|
|
55
|
+
excalidraw: 'json',
|
|
56
|
+
// Image formats — not highlight.js languages, but used for display detection
|
|
57
|
+
png: 'image', jpg: 'image', jpeg: 'image', gif: 'image',
|
|
58
|
+
webp: 'image', ico: 'image', bmp: 'image', tiff: 'image',
|
|
59
|
+
heic: 'image', avif: 'image'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Display overrides: when the hljs lang name isn't what users expect to see
|
|
63
|
+
const LANG_DISPLAY_OVERRIDE = { excalidraw: 'excalidraw' };
|
|
64
|
+
|
|
65
|
+
// Compound extension overrides (checked by filename, not just ext)
|
|
66
|
+
const COMPOUND_EXT_OVERRIDE = {
|
|
67
|
+
'.vl.json': { hlLang: 'json', displayLang: 'chart' },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function getLangForExt(ext, filename) {
|
|
71
|
+
// Check compound extensions first (e.g., .vl.json → chart)
|
|
72
|
+
if (filename) {
|
|
73
|
+
const lower = filename.toLowerCase();
|
|
74
|
+
for (const [compound, result] of Object.entries(COMPOUND_EXT_OVERRIDE)) {
|
|
75
|
+
if (lower.endsWith(compound)) return result;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let hlLang = LANG_MAP[ext] || 'plaintext';
|
|
79
|
+
// Fall back to plaintext for languages not in the hljs bundle (e.g. scala, sass)
|
|
80
|
+
if (hlLang !== 'plaintext' && hlLang !== 'image' && window.hljs && !window.hljs.getLanguage(hlLang)) {
|
|
81
|
+
hlLang = 'plaintext';
|
|
82
|
+
}
|
|
83
|
+
const displayLang = LANG_DISPLAY_OVERRIDE[ext] || (LANG_MAP[ext] || 'plaintext');
|
|
84
|
+
return { hlLang, displayLang };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert ANSI escape codes to HTML spans
|
|
89
|
+
*/
|
|
90
|
+
export function ansiToHtml(text) {
|
|
91
|
+
if (!text || !text.includes('\x1b[')) return null; // No ANSI codes
|
|
92
|
+
|
|
93
|
+
let result = '';
|
|
94
|
+
let currentClasses = [];
|
|
95
|
+
let i = 0;
|
|
96
|
+
|
|
97
|
+
while (i < text.length) {
|
|
98
|
+
// Check for ANSI escape sequence
|
|
99
|
+
if (text[i] === '\x1b' && text[i + 1] === '[') {
|
|
100
|
+
const endIdx = text.indexOf('m', i);
|
|
101
|
+
if (endIdx !== -1) {
|
|
102
|
+
const codes = text.slice(i + 2, endIdx).split(';');
|
|
103
|
+
|
|
104
|
+
// Close current span if any
|
|
105
|
+
if (currentClasses.length > 0) {
|
|
106
|
+
result += '</span>';
|
|
107
|
+
currentClasses = [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Process codes
|
|
111
|
+
for (const code of codes) {
|
|
112
|
+
if (code === '0' || code === '') {
|
|
113
|
+
// Reset
|
|
114
|
+
currentClasses = [];
|
|
115
|
+
} else if (code === '1') {
|
|
116
|
+
currentClasses.push('ansi-bold');
|
|
117
|
+
} else if (code === '3') {
|
|
118
|
+
currentClasses.push('ansi-italic');
|
|
119
|
+
} else if (code === '4') {
|
|
120
|
+
currentClasses.push('ansi-underline');
|
|
121
|
+
} else if (ANSI_COLORS[code]) {
|
|
122
|
+
currentClasses.push(ANSI_COLORS[code]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Open new span if we have classes
|
|
127
|
+
if (currentClasses.length > 0) {
|
|
128
|
+
result += `<span class="${currentClasses.join(' ')}">`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
i = endIdx + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Escape HTML special chars
|
|
137
|
+
const char = text[i];
|
|
138
|
+
if (char === '<') result += '<';
|
|
139
|
+
else if (char === '>') result += '>';
|
|
140
|
+
else if (char === '&') result += '&';
|
|
141
|
+
else result += char;
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Close any remaining span
|
|
146
|
+
if (currentClasses.length > 0) {
|
|
147
|
+
result += '</span>';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Apply pattern-based colorization to bash output
|
|
155
|
+
*/
|
|
156
|
+
export function colorizeBashLine(line, escapedLine) {
|
|
157
|
+
// Git diff patterns
|
|
158
|
+
if (/^\+(?!\+\+)/.test(line)) {
|
|
159
|
+
return `<span class="bash-diff-add">${escapedLine}</span>`;
|
|
160
|
+
}
|
|
161
|
+
if (/^-(?!--)/.test(line)) {
|
|
162
|
+
return `<span class="bash-diff-del">${escapedLine}</span>`;
|
|
163
|
+
}
|
|
164
|
+
if (/^@@\s/.test(line)) {
|
|
165
|
+
return `<span class="bash-diff-hunk">${escapedLine}</span>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Git status patterns
|
|
169
|
+
if (/^\s*M\s+/.test(line) || /^\s*modified:\s+/i.test(line)) {
|
|
170
|
+
return `<span class="bash-git-modified">${escapedLine}</span>`;
|
|
171
|
+
}
|
|
172
|
+
if (/^\s*A\s+/.test(line) || /^\s*new file:\s+/i.test(line)) {
|
|
173
|
+
return `<span class="bash-git-added">${escapedLine}</span>`;
|
|
174
|
+
}
|
|
175
|
+
if (/^\s*D\s+/.test(line) || /^\s*deleted:\s+/i.test(line)) {
|
|
176
|
+
return `<span class="bash-git-deleted">${escapedLine}</span>`;
|
|
177
|
+
}
|
|
178
|
+
if (/^\?\?\s+/.test(line)) {
|
|
179
|
+
return `<span class="bash-git-untracked">${escapedLine}</span>`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Error patterns
|
|
183
|
+
if (/\b(error|Error|ERROR|fatal|FATAL|fail|FAIL|failed|FAILED)\b/.test(line)) {
|
|
184
|
+
return `<span class="bash-error-line">${escapedLine}</span>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Warning patterns
|
|
188
|
+
if (/\b(warning|Warning|WARNING|warn|WARN)\b/.test(line)) {
|
|
189
|
+
return `<span class="bash-warn-line">${escapedLine}</span>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Success patterns
|
|
193
|
+
if (/\b(success|Success|SUCCESS|passed|PASSED|ok|OK|done|Done|DONE)\b/.test(line)) {
|
|
194
|
+
return `<span class="bash-success-line">${escapedLine}</span>`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return escapedLine;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* ToolRenderer - Renders tool use blocks with specialized formatting
|
|
202
|
+
*/
|
|
203
|
+
export class ToolRenderer {
|
|
204
|
+
/**
|
|
205
|
+
* @param {Object} options - Configuration
|
|
206
|
+
* @param {number} options.maxOutputLength - Max output chars before truncation
|
|
207
|
+
*/
|
|
208
|
+
constructor(options = {}) {
|
|
209
|
+
this.maxOutputLength = options.maxOutputLength || CONFIG.MAX_OUTPUT_LENGTH || 3000;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate gutter icon HTML for tool collapse
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_getGutterIconHtml(toolId) {
|
|
217
|
+
return `
|
|
218
|
+
<button class="tool-gutter-icon" data-act="toggle-normal-tool-collapse" data-id="${escapeAttr(toolId)}" data-tooltip="Collapse/expand this tool">
|
|
219
|
+
<svg class="gutter-collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
220
|
+
<path d="M19 9l-7 7-7-7"/>
|
|
221
|
+
</svg>
|
|
222
|
+
<svg class="gutter-expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
223
|
+
<path d="M9 5l7 7-7 7"/>
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Wrap tool HTML with a container that has gutter icon
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
_wrapWithGutter(toolHtml, toolId, { collapseMode = 'compact', noGutterIcon = false } = {}) {
|
|
234
|
+
const gutterIcon = noGutterIcon ? '' : this._getGutterIconHtml(toolId);
|
|
235
|
+
const collapsedClass = collapseMode === 'collapsed' ? ' tool-collapsed' : '';
|
|
236
|
+
return `<div class="tool-block-wrapper${collapsedClass}" data-tool-id="${toolId}">${gutterIcon}${toolHtml}</div>`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Render a tool block for a message
|
|
241
|
+
*/
|
|
242
|
+
renderToolBlock(msg, { isThinking = false, isAgent = false } = {}) {
|
|
243
|
+
const toolId = `tool-${msg.id}`;
|
|
244
|
+
const toolType = getToolCategory(msg.toolName);
|
|
245
|
+
const context = isAgent ? 'agent' : isThinking ? 'thinking' : 'normal';
|
|
246
|
+
const mode = getToolCollapseMode(context, toolType);
|
|
247
|
+
const defaultExpanded = mode === 'expanded';
|
|
248
|
+
const gutterOpts = { collapseMode: mode };
|
|
249
|
+
let html;
|
|
250
|
+
|
|
251
|
+
// Special rendering for Edit tool - show colorized diff
|
|
252
|
+
if (msg.toolName === 'Edit' && msg.toolInput) {
|
|
253
|
+
// Check for sibling tool error
|
|
254
|
+
const editCleaned = cleanToolError(msg.toolOutput || msg.toolError || '');
|
|
255
|
+
if (editCleaned.isSiblingError) {
|
|
256
|
+
const fileName = (msg.toolInput.file_path || '').split('/').pop();
|
|
257
|
+
html = this._renderSiblingErrorBlock(msg, 'edit', escapeHtml(fileName));
|
|
258
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
259
|
+
}
|
|
260
|
+
const diffHtml = this.renderEditDiff(msg.toolInput, msg.toolOutput, msg.toolId, msg.startLine);
|
|
261
|
+
if (diffHtml) {
|
|
262
|
+
const hasOutput = msg.toolOutput || msg.toolError;
|
|
263
|
+
const isCompleted = msg.toolCompleted || hasOutput;
|
|
264
|
+
const cleanedEditErr = cleanToolError(msg.toolError);
|
|
265
|
+
const statusClass = msg.toolError ? 'edit-status-error' : (isCompleted ? 'edit-status-success' : 'edit-status-pending');
|
|
266
|
+
const statusText = msg.toolError ? 'Failed' : (isCompleted ? 'Applied' : 'Applying...');
|
|
267
|
+
|
|
268
|
+
html = `<div class="tool-block edit-tool-block" id="${toolId}">
|
|
269
|
+
${diffHtml}
|
|
270
|
+
<div class="edit-status ${statusClass}">${statusText}${msg.toolError ? ': ' + escapeHtml(cleanedEditErr.text) : ''}</div>
|
|
271
|
+
</div>`;
|
|
272
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Special rendering for TodoWrite tool - show nice todo list
|
|
277
|
+
if (msg.toolName === 'TodoWrite' && msg.toolInput?.todos) {
|
|
278
|
+
html = this.renderTodoBlock(msg);
|
|
279
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Special rendering for Read tool with image files - add View button
|
|
283
|
+
if (msg.toolName === 'Read' && msg.toolInput?.file_path) {
|
|
284
|
+
const filePath = msg.toolInput.file_path;
|
|
285
|
+
const isImage = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i.test(filePath);
|
|
286
|
+
if (isImage) {
|
|
287
|
+
html = this.renderReadImageBlock(msg);
|
|
288
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
289
|
+
}
|
|
290
|
+
// Non-image Read tool - always use styled block (handles loading state too)
|
|
291
|
+
html = this.renderReadBlock(msg, { defaultExpanded });
|
|
292
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Specialized compact rendering for common tools
|
|
296
|
+
if ((msg.toolName === 'Bash' || msg.toolName === 'Shell') && msg.toolInput?.command) {
|
|
297
|
+
html = this.renderBashBlock(msg, { defaultExpanded });
|
|
298
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
299
|
+
}
|
|
300
|
+
if (msg.toolName === 'Grep' && msg.toolInput?.pattern) {
|
|
301
|
+
html = this.renderGrepBlock(msg, { defaultExpanded });
|
|
302
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
303
|
+
}
|
|
304
|
+
if (msg.toolName === 'Glob' && msg.toolInput?.pattern) {
|
|
305
|
+
html = this.renderGlobBlock(msg, { defaultExpanded });
|
|
306
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
307
|
+
}
|
|
308
|
+
if (msg.toolName === 'Write' && msg.toolInput?.file_path) {
|
|
309
|
+
html = this.renderWriteBlock(msg, { defaultExpanded });
|
|
310
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
311
|
+
}
|
|
312
|
+
if (msg.toolName === 'WebFetch' || msg.toolName === 'WebSearch') {
|
|
313
|
+
html = this.renderWebFetchBlock(msg, { defaultExpanded });
|
|
314
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Special rendering for Task (agent) tool - show grouped style
|
|
318
|
+
// No gutter icon on Task block itself; sub-agent tools get their own gutter icons
|
|
319
|
+
if (msg.toolName === 'Task') {
|
|
320
|
+
html = this.renderTaskBlock(msg);
|
|
321
|
+
return this._wrapWithGutter(html, toolId, { collapseMode: mode, noGutterIcon: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Compact rendering for Skill tool
|
|
325
|
+
if (msg.toolName === 'Skill') {
|
|
326
|
+
html = this.renderSkillBlock(msg);
|
|
327
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Compact rendering for TaskOutput tool
|
|
331
|
+
if (msg.toolName === 'TaskOutput') {
|
|
332
|
+
html = this.renderTaskOutputBlock(msg);
|
|
333
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Compact rendering for TaskStop tool
|
|
337
|
+
if (msg.toolName === 'TaskStop') {
|
|
338
|
+
html = this.renderTaskStopBlock(msg);
|
|
339
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Compact rendering for EnterPlanMode
|
|
343
|
+
if (msg.toolName === 'EnterPlanMode') {
|
|
344
|
+
html = this.renderEnterPlanBlock(msg);
|
|
345
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Compact rendering for AskUserQuestion (safety net — normally rendered as interactive form)
|
|
349
|
+
if (msg.toolName === 'AskUserQuestion') {
|
|
350
|
+
const questions = msg.toolInput?.questions || [];
|
|
351
|
+
const label = questions.length === 1
|
|
352
|
+
? escapeHtml(questions[0].question || 'Question')
|
|
353
|
+
: `${questions.length} questions`;
|
|
354
|
+
html = `<div class="tool-block compact-tool-block" id="${toolId}">
|
|
355
|
+
<div class="tool-header compact-header"><span class="tool-name compact-tool-name">
|
|
356
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
357
|
+
AskUserQuestion<span class="tool-header-path">${label}</span></span></div></div>`;
|
|
358
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Compact rendering for ExitPlanMode (safety net — normally rendered as plan approval card)
|
|
362
|
+
if (msg.toolName === 'ExitPlanMode') {
|
|
363
|
+
html = `<div class="tool-block compact-tool-block" id="${toolId}">
|
|
364
|
+
<div class="tool-header compact-header"><span class="tool-name compact-tool-name">
|
|
365
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
|
366
|
+
ExitPlanMode<span class="tool-header-path">Plan ready for approval</span></span></div></div>`;
|
|
367
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Default tool block rendering
|
|
371
|
+
const inputDisplay = this.formatToolInput(msg.toolName, msg.toolInput);
|
|
372
|
+
const hasOutput = msg.toolOutput || msg.toolError;
|
|
373
|
+
const isCompleted = msg.toolCompleted || hasOutput;
|
|
374
|
+
const defaultCleaned = cleanToolError(msg.toolOutput || msg.toolError || '');
|
|
375
|
+
const outputContent = hasOutput
|
|
376
|
+
? (defaultCleaned.isSiblingError
|
|
377
|
+
? `<span class="bash-sibling-label">${S.tool_renderer.status.skipped}</span> a parallel tool call failed`
|
|
378
|
+
: this.linkifyToolOutput(escapeHtml(this.truncateOutput(defaultCleaned.text)), msg.fileLinks))
|
|
379
|
+
: (isCompleted ? S.tool_renderer.badges.done : '');
|
|
380
|
+
const outputClass = `tool-output${msg.toolError ? ' error' : ''}${(msg.toolOutput?.length || 0) > this.maxOutputLength ? ' truncated' : ''}`;
|
|
381
|
+
|
|
382
|
+
// Get compact header preview for all tools
|
|
383
|
+
const headerPreview = this.getToolHeaderPreview(msg.toolName, msg.toolInput);
|
|
384
|
+
|
|
385
|
+
// Add buttons for file-related tools (Read, Write, Edit)
|
|
386
|
+
const hasFilePath = msg.toolInput?.file_path;
|
|
387
|
+
const isFileViewable = (msg.toolName === 'Read' || msg.toolName === 'Write' || msg.toolName === 'Edit') && hasFilePath;
|
|
388
|
+
// Model-controlled path: stash quote-safe in data-file and read it back
|
|
389
|
+
// via this.dataset.file at click time (never interpolated into the JS
|
|
390
|
+
// string), so a path with `"`/`'` can't break out. Same pattern as fileLink().
|
|
391
|
+
const filePathAttr = hasFilePath ? escapeAttr(msg.toolInput.file_path) : '';
|
|
392
|
+
|
|
393
|
+
// "Open in Editor" button - opens file in editor tab
|
|
394
|
+
const editorButton = isFileViewable
|
|
395
|
+
? `<button class="tool-action-btn" data-act="open-in-editor" data-file="${filePathAttr}" data-tooltip="Open in editor">
|
|
396
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 9.5-9.5z"/></svg>
|
|
397
|
+
</button>`
|
|
398
|
+
: '';
|
|
399
|
+
|
|
400
|
+
// "View" button - opens file preview widget
|
|
401
|
+
const viewButton = isFileViewable
|
|
402
|
+
? `<button class="tool-action-btn" data-act="preview-file" data-file="${filePathAttr}" data-tooltip="Preview file">
|
|
403
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
404
|
+
</button>`
|
|
405
|
+
: '';
|
|
406
|
+
|
|
407
|
+
html = `<div class="tool-block" id="${toolId}">
|
|
408
|
+
<div class="tool-header" data-act="toggle-tool" data-id="${escapeAttr(msg.id)}">
|
|
409
|
+
<span class="tool-name">
|
|
410
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
|
|
411
|
+
${escapeHtml(msg.toolName)}${headerPreview ? `<span class="tool-header-path">${headerPreview}</span>` : ''}
|
|
412
|
+
</span>
|
|
413
|
+
<div class="tool-actions">
|
|
414
|
+
${editorButton}${viewButton}<button class="tool-action-btn" data-act="copy-tool-output" data-id="${escapeAttr(msg.id)}" data-tooltip="Copy">
|
|
415
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="tool-content">
|
|
420
|
+
<div class="tool-input">
|
|
421
|
+
${inputDisplay}
|
|
422
|
+
</div>
|
|
423
|
+
${isCompleted ? `<div class="${outputClass}">${outputContent}</div>` : '<div class="tool-output" style="color: var(--text-muted)">Running...</div>'}
|
|
424
|
+
</div>
|
|
425
|
+
</div>`;
|
|
426
|
+
return this._wrapWithGutter(html, toolId, gutterOpts);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get a compact preview string for the tool header
|
|
431
|
+
*/
|
|
432
|
+
getToolHeaderPreview(toolName, input) {
|
|
433
|
+
if (!input) return '';
|
|
434
|
+
|
|
435
|
+
let preview = '';
|
|
436
|
+
switch (toolName) {
|
|
437
|
+
case 'Bash':
|
|
438
|
+
case 'Shell':
|
|
439
|
+
preview = input.command || '';
|
|
440
|
+
break;
|
|
441
|
+
case 'Read':
|
|
442
|
+
// Show just filename
|
|
443
|
+
preview = basename(input.file_path) || '';
|
|
444
|
+
break;
|
|
445
|
+
case 'Write':
|
|
446
|
+
preview = basename(input.file_path) || '';
|
|
447
|
+
break;
|
|
448
|
+
case 'Grep':
|
|
449
|
+
preview = `/${input.pattern || ''}/`;
|
|
450
|
+
break;
|
|
451
|
+
case 'Glob':
|
|
452
|
+
preview = input.pattern || '';
|
|
453
|
+
break;
|
|
454
|
+
case 'WebFetch':
|
|
455
|
+
// Show domain only
|
|
456
|
+
try {
|
|
457
|
+
preview = new URL(input.url).hostname;
|
|
458
|
+
} catch { preview = input.url || ''; }
|
|
459
|
+
break;
|
|
460
|
+
case 'Task':
|
|
461
|
+
preview = input.description || '';
|
|
462
|
+
break;
|
|
463
|
+
case 'Skill':
|
|
464
|
+
preview = input.skill || '';
|
|
465
|
+
break;
|
|
466
|
+
default:
|
|
467
|
+
// Try common field names
|
|
468
|
+
preview = input.description || input.command || input.path || basename(input.file_path) || '';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Truncate and escape
|
|
472
|
+
if (preview.length > 50) {
|
|
473
|
+
preview = preview.substring(0, 47) + '...';
|
|
474
|
+
}
|
|
475
|
+
return escapeHtml(preview);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Generate a clickable filename link for tool headers.
|
|
480
|
+
* Returns an <a> with file-path-link class so the context menu system picks it up.
|
|
481
|
+
* @param {string} fullPath - Full file path
|
|
482
|
+
* @param {string} displayName - Display text (usually just filename)
|
|
483
|
+
* @param {string} extraClass - Additional CSS class (e.g., 'read-filename')
|
|
484
|
+
*/
|
|
485
|
+
fileLink(fullPath, displayName, extraClass = '') {
|
|
486
|
+
// SECURITY: fullPath is model-controlled (e.g. a Write/Edit target shown
|
|
487
|
+
// on the permission-approval card). Keep it out of inline JS entirely —
|
|
488
|
+
// stash it in a quote-escaped data-file attribute and read it back via
|
|
489
|
+
// this.dataset.file at click time, so a path containing `"`/`'` can't
|
|
490
|
+
// break out of the attribute or the onclick handler. escapeAttr covers
|
|
491
|
+
// quotes; escapeHtml only covers & < >.
|
|
492
|
+
const attrPath = escapeAttr(fullPath);
|
|
493
|
+
const classes = `file-path-link${extraClass ? ' ' + extraClass : ''}`;
|
|
494
|
+
return `<a href="#" class="${classes}" data-act="preview-file" data-file="${attrPath}" data-tooltip="${attrPath}">${escapeHtml(displayName)}</a>`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Format tool input for display
|
|
499
|
+
*/
|
|
500
|
+
formatToolInput(toolName, input) {
|
|
501
|
+
if (!input) return '';
|
|
502
|
+
|
|
503
|
+
if ((toolName === 'Bash' || toolName === 'Shell') && input.command) {
|
|
504
|
+
return `<code>$ ${escapeHtml(input.command)}</code>`;
|
|
505
|
+
}
|
|
506
|
+
if (toolName === 'Read' && input.file_path) {
|
|
507
|
+
return `<code>${escapeHtml(input.file_path)}</code>`;
|
|
508
|
+
}
|
|
509
|
+
if (toolName === 'Grep' && input.pattern) {
|
|
510
|
+
return `<code>/${escapeHtml(input.pattern)}/</code>`;
|
|
511
|
+
}
|
|
512
|
+
if (toolName === 'Glob' && input.pattern) {
|
|
513
|
+
return `<code>${escapeHtml(input.pattern)}</code>`;
|
|
514
|
+
}
|
|
515
|
+
if (toolName === 'Write' && input.file_path) {
|
|
516
|
+
return `<code>${escapeHtml(input.file_path)}</code>`;
|
|
517
|
+
}
|
|
518
|
+
if (toolName === 'Edit' && input.file_path) {
|
|
519
|
+
return `<code>${escapeHtml(input.file_path)}</code>`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return this._renderInputTable(input);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Render a tool input object as a key→value table.
|
|
527
|
+
* Top-level keys are rows; nested objects/arrays are JSON-stringified inline.
|
|
528
|
+
*/
|
|
529
|
+
_renderInputTable(input) {
|
|
530
|
+
if (typeof input !== 'object' || Array.isArray(input)) {
|
|
531
|
+
return `<pre>${escapeHtml(JSON.stringify(input, null, 2))}</pre>`;
|
|
532
|
+
}
|
|
533
|
+
const keys = Object.keys(input);
|
|
534
|
+
if (keys.length === 0) return '';
|
|
535
|
+
|
|
536
|
+
const rows = keys.map(key => {
|
|
537
|
+
const value = input[key];
|
|
538
|
+
let cellHtml;
|
|
539
|
+
if (value === null) {
|
|
540
|
+
cellHtml = '<span class="tool-input-null">null</span>';
|
|
541
|
+
} else if (typeof value === 'string') {
|
|
542
|
+
cellHtml = escapeHtml(value);
|
|
543
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
544
|
+
cellHtml = `<span class="tool-input-scalar">${escapeHtml(String(value))}</span>`;
|
|
545
|
+
} else {
|
|
546
|
+
cellHtml = `<code class="tool-input-nested">${escapeHtml(JSON.stringify(value))}</code>`;
|
|
547
|
+
}
|
|
548
|
+
return `<tr><th class="tool-input-key">${escapeHtml(key)}</th><td class="tool-input-value">${cellHtml}</td></tr>`;
|
|
549
|
+
}).join('');
|
|
550
|
+
|
|
551
|
+
return `<table class="tool-input-table">${rows}</table>`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Format tool input for thinking message (compact display)
|
|
556
|
+
*/
|
|
557
|
+
formatThinkingToolInput(toolName, input) {
|
|
558
|
+
if (!input) return '';
|
|
559
|
+
|
|
560
|
+
if ((toolName === 'Bash' || toolName === 'Shell') && input.command) {
|
|
561
|
+
return `<code>$ ${escapeHtml(input.command.slice(0, 60))}${input.command.length > 60 ? '...' : ''}</code>`;
|
|
562
|
+
}
|
|
563
|
+
if (toolName === 'Read' && input.file_path) {
|
|
564
|
+
return `<code>${escapeHtml(basename(input.file_path))}</code>`;
|
|
565
|
+
}
|
|
566
|
+
if (toolName === 'Write' && input.file_path) {
|
|
567
|
+
return `<code>${escapeHtml(basename(input.file_path))}</code>`;
|
|
568
|
+
}
|
|
569
|
+
if (toolName === 'Edit' && input.file_path) {
|
|
570
|
+
return `<code>${escapeHtml(basename(input.file_path))}</code>`;
|
|
571
|
+
}
|
|
572
|
+
if (toolName === 'Grep' && input.pattern) {
|
|
573
|
+
return `<code>/${escapeHtml(input.pattern.slice(0, 30))}${input.pattern.length > 30 ? '...' : ''}/</code>`;
|
|
574
|
+
}
|
|
575
|
+
if (toolName === 'Glob' && input.pattern) {
|
|
576
|
+
return `<code>${escapeHtml(input.pattern)}</code>`;
|
|
577
|
+
}
|
|
578
|
+
if (toolName === 'Task') {
|
|
579
|
+
return `<span class="thinking-tool-desc">${escapeHtml(input.description || '')}</span>`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// For other tools, show first key-value pair
|
|
583
|
+
const keys = Object.keys(input);
|
|
584
|
+
if (keys.length > 0) {
|
|
585
|
+
const key = keys[0];
|
|
586
|
+
const val = String(input[key]).slice(0, 40);
|
|
587
|
+
return `<span class="thinking-tool-param">${key}: ${escapeHtml(val)}${String(input[key]).length > 40 ? '...' : ''}</span>`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return '';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Linkify file paths and URLs in tool output (already HTML-escaped text).
|
|
595
|
+
* Supports two modes:
|
|
596
|
+
* 1. Position-based: Uses server-provided positions (fileLinks array)
|
|
597
|
+
* 2. Client-scan: Scans text for paths and verifies against a map (verifiedFiles object)
|
|
598
|
+
* @param {string} text - The HTML-escaped tool output text
|
|
599
|
+
* @param {Array|null} fileLinks - Server-provided file links [{path, resolved, start, end, line_info}]
|
|
600
|
+
* @param {Object|null} verifiedFiles - Map of {path: resolvedPath} for client-side scanning
|
|
601
|
+
*/
|
|
602
|
+
linkifyToolOutput(text, fileLinks = null, verifiedFiles = null) {
|
|
603
|
+
const replacements = []; // [{start, end, html}]
|
|
604
|
+
|
|
605
|
+
// 1. Find URLs (scanned client-side, no server verification needed)
|
|
606
|
+
const urlPattern = buildUrlPattern();
|
|
607
|
+
let match;
|
|
608
|
+
while ((match = urlPattern.exec(text)) !== null) {
|
|
609
|
+
let url = match[0];
|
|
610
|
+
const cleaned = cleanUrlTrailingPunct(url);
|
|
611
|
+
url = cleaned.url;
|
|
612
|
+
const end = match.index + url.length;
|
|
613
|
+
|
|
614
|
+
replacements.push({
|
|
615
|
+
start: match.index,
|
|
616
|
+
end: end,
|
|
617
|
+
// href quote-safe; text escaped. (The URL regex already
|
|
618
|
+
// constrains the scheme to http/https.)
|
|
619
|
+
html: `<a href="${escapeAttr(url)}" class="external-link" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 2a. Add file links using server-provided positions (when available)
|
|
624
|
+
if (fileLinks && Array.isArray(fileLinks)) {
|
|
625
|
+
for (const link of fileLinks) {
|
|
626
|
+
const { path, resolved, start, end, line_info } = link;
|
|
627
|
+
|
|
628
|
+
// Check for overlap with URL replacements
|
|
629
|
+
const overlaps = replacements.some(r =>
|
|
630
|
+
(start < r.end && end > r.start)
|
|
631
|
+
);
|
|
632
|
+
if (overlaps) continue;
|
|
633
|
+
|
|
634
|
+
const fullDisplay = line_info ? path + line_info : path;
|
|
635
|
+
// Preview path (resolved || path) is read at click time from
|
|
636
|
+
// this.dataset — never interpolated into the JS string.
|
|
637
|
+
const resolvedAttr = resolved ? ` data-resolved="${escapeAttr(resolved)}"` : '';
|
|
638
|
+
|
|
639
|
+
// Parse line info into options for preview
|
|
640
|
+
const lineOpts = parseLineInfo(line_info);
|
|
641
|
+
const optsJson = lineOpts ? JSON.stringify(lineOpts).replace(/"/g, '"') : '';
|
|
642
|
+
const optsAttr = optsJson ? ` data-line-opts="${optsJson}"` : '';
|
|
643
|
+
|
|
644
|
+
replacements.push({
|
|
645
|
+
start,
|
|
646
|
+
end,
|
|
647
|
+
html: `<a href="#" class="file-path-link" data-act="open-file-link" data-file="${escapeAttr(path)}"${resolvedAttr} data-tooltip="${escapeAttr(resolved || path)}"${optsAttr}>${escapeHtml(fullDisplay)}</a>`
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// 2b. Client-side path scanning (for colorized output where positions don't work)
|
|
653
|
+
if (verifiedFiles && Object.keys(verifiedFiles).length > 0) {
|
|
654
|
+
// Helper to add a verified path replacement
|
|
655
|
+
const addPathReplacement = (matchObj, path, lineInfo, fullMatch) => {
|
|
656
|
+
const resolvedPath = verifiedFiles[path];
|
|
657
|
+
if (!resolvedPath) return;
|
|
658
|
+
|
|
659
|
+
// Check for overlap with existing replacements
|
|
660
|
+
const overlaps = replacements.some(r =>
|
|
661
|
+
(matchObj.index < r.end && matchObj.index + fullMatch.length > r.start)
|
|
662
|
+
);
|
|
663
|
+
if (overlaps) return;
|
|
664
|
+
|
|
665
|
+
const fullDisplay = lineInfo ? path + lineInfo : path;
|
|
666
|
+
|
|
667
|
+
const lineOpts = parseLineInfo(lineInfo);
|
|
668
|
+
const optsJson = lineOpts ? JSON.stringify(lineOpts).replace(/"/g, '"') : '';
|
|
669
|
+
const optsAttr = optsJson ? ` data-line-opts="${optsJson}"` : '';
|
|
670
|
+
|
|
671
|
+
replacements.push({
|
|
672
|
+
start: matchObj.index,
|
|
673
|
+
end: matchObj.index + fullMatch.length,
|
|
674
|
+
html: `<a href="#" class="file-path-link" data-act="open-file-link" data-file="${escapeAttr(path)}" data-resolved="${escapeAttr(resolvedPath)}" data-tooltip="${escapeAttr(resolvedPath)}"${optsAttr}>${escapeHtml(fullDisplay)}</a>`
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// Scan for paths with directories (e.g., static/js/app.js)
|
|
679
|
+
const pathPattern = buildPathPattern();
|
|
680
|
+
while ((match = pathPattern.exec(text)) !== null) {
|
|
681
|
+
const path = match[1];
|
|
682
|
+
const lineInfo = match[2] || '';
|
|
683
|
+
|
|
684
|
+
if (!path.includes('/')) continue;
|
|
685
|
+
if (/^\d+\.\d+/.test(path)) continue;
|
|
686
|
+
if (/^\d+\/\d+/.test(path)) continue;
|
|
687
|
+
|
|
688
|
+
addPathReplacement(match, path, lineInfo, match[0]);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Scan for standalone filenames (e.g., CLAUDE.md, server.py)
|
|
692
|
+
const standalonePattern = buildStandalonePattern();
|
|
693
|
+
while ((match = standalonePattern.exec(text)) !== null) {
|
|
694
|
+
const filename = match[1];
|
|
695
|
+
const lineInfo = match[2] || '';
|
|
696
|
+
|
|
697
|
+
if (!isValidStandaloneFile(filename, text.slice(Math.max(0, match.index - 20), match.index))) continue;
|
|
698
|
+
|
|
699
|
+
addPathReplacement(match, filename, lineInfo, match[0]);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// 3. Sort by position (descending) and apply replacements
|
|
704
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
705
|
+
for (const r of replacements) {
|
|
706
|
+
text = text.slice(0, r.start) + r.html + text.slice(r.end);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return text;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Render a colorized diff for Edit tool operations.
|
|
714
|
+
* Shows removed lines in red, added lines in green, with stats and file link.
|
|
715
|
+
* @param {object} input - Tool input with file_path, old_string, new_string
|
|
716
|
+
* @param {string} toolOutput - Tool output for line number parsing
|
|
717
|
+
* @param {string} [toolId] - Tool ID for linking to Changes panel
|
|
718
|
+
*/
|
|
719
|
+
parseEditLineNumber(toolOutput, newString) {
|
|
720
|
+
if (!toolOutput || !newString) return null;
|
|
721
|
+
|
|
722
|
+
// Extract "NUMBER<delim>content" pairs — Claude CLI uses either → (older) or \t (current)
|
|
723
|
+
const linePattern = /^\s*(\d+)(?:→|\t)(.*)$/gm;
|
|
724
|
+
const firstNewLine = newString.split('\n')[0];
|
|
725
|
+
|
|
726
|
+
let match;
|
|
727
|
+
while ((match = linePattern.exec(toolOutput)) !== null) {
|
|
728
|
+
const lineNum = parseInt(match[1], 10);
|
|
729
|
+
const content = match[2];
|
|
730
|
+
// Check if this line matches first line of new_string (trim to handle padding)
|
|
731
|
+
if (content.trim() === firstNewLine.trim()) {
|
|
732
|
+
return lineNum;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return null; // Unknown — caller hides line numbers rather than faking 1-based ones
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Render a Read tool block for image files with inline preview
|
|
741
|
+
*/
|
|
742
|
+
_renderSiblingErrorBlock(msg, toolType, label) {
|
|
743
|
+
const icons = {
|
|
744
|
+
grep: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
|
745
|
+
glob: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
|
|
746
|
+
read: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>',
|
|
747
|
+
write: '<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/>',
|
|
748
|
+
edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
|
749
|
+
};
|
|
750
|
+
const icon = icons[toolType] || icons.read;
|
|
751
|
+
return `<div class="tool-block ${toolType}-block sibling-error" id="tool-${msg.id}">
|
|
752
|
+
<div class="${toolType}-header">
|
|
753
|
+
<svg class="${toolType}-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">${icon}</svg>
|
|
754
|
+
<code class="${toolType}-pattern">${label}</code>
|
|
755
|
+
<span class="sibling-error-badge">Skipped</span>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="sibling-error-msg">A parallel tool call failed</div>
|
|
758
|
+
</div>`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Render a compact Bash/Shell tool block
|
|
763
|
+
* Design: Clickable command copies, expandable for long/multiline commands
|
|
764
|
+
*/
|
|
765
|
+
_parseTaskOutputXml(text) {
|
|
766
|
+
if (!text) return {};
|
|
767
|
+
const result = {};
|
|
768
|
+
const tagRegex = /<(\w+)>([\s\S]*?)<\/\1>/g;
|
|
769
|
+
let match;
|
|
770
|
+
while ((match = tagRegex.exec(text)) !== null) {
|
|
771
|
+
result[match[1]] = match[2].trim();
|
|
772
|
+
}
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Parse agent streaming JSONL output into useful components.
|
|
778
|
+
* Extracts slug, text responses, tool counts, and token usage.
|
|
779
|
+
*/
|
|
780
|
+
_parseAgentStream(rawOutput) {
|
|
781
|
+
const result = {
|
|
782
|
+
slug: '',
|
|
783
|
+
texts: [],
|
|
784
|
+
toolCounts: {},
|
|
785
|
+
totalInputTokens: 0,
|
|
786
|
+
totalOutputTokens: 0,
|
|
787
|
+
isTruncated: false,
|
|
788
|
+
truncatedPath: '',
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
if (!rawOutput) return result;
|
|
792
|
+
|
|
793
|
+
// Check for truncation marker at start
|
|
794
|
+
const truncMatch = rawOutput.match(/^\[Truncated\.\s*Full output:\s*(.+?)\]/);
|
|
795
|
+
if (truncMatch) {
|
|
796
|
+
result.isTruncated = true;
|
|
797
|
+
result.truncatedPath = truncMatch[1];
|
|
798
|
+
rawOutput = rawOutput.slice(truncMatch[0].length);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const lines = rawOutput.split('\n');
|
|
802
|
+
for (const line of lines) {
|
|
803
|
+
const trimmed = line.trim();
|
|
804
|
+
if (!trimmed || !trimmed.startsWith('{')) continue;
|
|
805
|
+
|
|
806
|
+
try {
|
|
807
|
+
const obj = JSON.parse(trimmed);
|
|
808
|
+
|
|
809
|
+
// Extract slug from first occurrence
|
|
810
|
+
if (obj.slug && !result.slug) {
|
|
811
|
+
result.slug = obj.slug;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Parse assistant messages for text and tool_use
|
|
815
|
+
if (obj.message?.role === 'assistant' && obj.message?.content) {
|
|
816
|
+
for (const block of obj.message.content) {
|
|
817
|
+
if (block.type === 'text' && block.text) {
|
|
818
|
+
result.texts.push(block.text);
|
|
819
|
+
}
|
|
820
|
+
if (block.type === 'tool_use' && block.name) {
|
|
821
|
+
result.toolCounts[block.name] = (result.toolCounts[block.name] || 0) + 1;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Accumulate token usage
|
|
827
|
+
if (obj.message?.usage) {
|
|
828
|
+
const u = obj.message.usage;
|
|
829
|
+
result.totalOutputTokens += u.output_tokens || 0;
|
|
830
|
+
result.totalInputTokens += (u.input_tokens || 0) +
|
|
831
|
+
(u.cache_read_input_tokens || 0) +
|
|
832
|
+
(u.cache_creation_input_tokens || 0);
|
|
833
|
+
}
|
|
834
|
+
} catch {
|
|
835
|
+
// Malformed JSON line (possibly truncated mid-stream), skip
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
_parseTaskOutputWithUsage(toolOutput) {
|
|
843
|
+
const raw = this._parseTaskOutput(toolOutput);
|
|
844
|
+
return parseTaskUsage(raw);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Parse Task toolOutput to extract text content
|
|
849
|
+
* Output format is usually: [{"type":"text","text":"..."}]
|
|
850
|
+
*/
|
|
851
|
+
_parseTaskOutput(toolOutput) {
|
|
852
|
+
if (!toolOutput) return '';
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
let parsed = toolOutput;
|
|
856
|
+
if (typeof toolOutput === 'string') {
|
|
857
|
+
if (toolOutput.startsWith('[')) {
|
|
858
|
+
parsed = JSON.parse(toolOutput);
|
|
859
|
+
} else {
|
|
860
|
+
return toolOutput;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (Array.isArray(parsed)) {
|
|
865
|
+
return parsed
|
|
866
|
+
.filter(block => block.type === 'text' && block.text)
|
|
867
|
+
.map(block => block.text)
|
|
868
|
+
.join('\n\n');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (parsed && typeof parsed === 'object' && parsed.text) {
|
|
872
|
+
return parsed.text;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
|
|
876
|
+
} catch (e) {
|
|
877
|
+
return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Strip markdown syntax for clean preview text
|
|
883
|
+
*/
|
|
884
|
+
_stripMarkdownForPreview(text) {
|
|
885
|
+
if (!text) return '';
|
|
886
|
+
return text
|
|
887
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
888
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
889
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
890
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
891
|
+
.replace(/_([^_]+)_/g, '$1')
|
|
892
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
893
|
+
.replace(/^>\s*/gm, '')
|
|
894
|
+
.replace(/^[-*+]\s+/gm, '')
|
|
895
|
+
.replace(/^\d+\.\s+/gm, '')
|
|
896
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
897
|
+
.replace(/\s+/g, ' ')
|
|
898
|
+
.trim();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Render markdown content safely
|
|
903
|
+
*/
|
|
904
|
+
_renderMarkdownSafe(content) {
|
|
905
|
+
if (!content) return '';
|
|
906
|
+
// Try to use app's markdown renderer if available
|
|
907
|
+
if (window.app?.markdown) {
|
|
908
|
+
return window.app.markdown.render(content);
|
|
909
|
+
}
|
|
910
|
+
// Fallback: escape and preserve whitespace
|
|
911
|
+
return `<pre style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Update a Task block result while preserving nested children
|
|
916
|
+
* Called when Task completes to show response without losing nested tools
|
|
917
|
+
*/
|
|
918
|
+
_updateTaskBlockResult(taskBlock, msg) {
|
|
919
|
+
const isError = !!msg.toolError;
|
|
920
|
+
const isCompleted = msg.toolCompleted || msg.toolOutput || isError;
|
|
921
|
+
|
|
922
|
+
// Remove live progress indicator (replaced by final result)
|
|
923
|
+
const progressEl = taskBlock.querySelector('.task-progress');
|
|
924
|
+
if (progressEl) progressEl.remove();
|
|
925
|
+
|
|
926
|
+
// Update data-complete attribute
|
|
927
|
+
taskBlock.setAttribute('data-complete', isCompleted ? 'true' : 'false');
|
|
928
|
+
|
|
929
|
+
// Update status badge
|
|
930
|
+
const badge = taskBlock.querySelector('.task-block-badge');
|
|
931
|
+
if (badge) {
|
|
932
|
+
if (isError) {
|
|
933
|
+
badge.className = 'task-block-badge task-block-badge-error';
|
|
934
|
+
badge.textContent = S.tool_renderer.badges.failed;
|
|
935
|
+
} else if (isCompleted) {
|
|
936
|
+
badge.className = 'task-block-badge task-block-badge-success';
|
|
937
|
+
badge.textContent = S.tool_renderer.badges.done;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Parse agent response and usage data
|
|
942
|
+
const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(msg.toolOutput);
|
|
943
|
+
|
|
944
|
+
// Inject usage badges into header
|
|
945
|
+
if (usage) {
|
|
946
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
947
|
+
if (header) {
|
|
948
|
+
header.querySelectorAll('.task-block-tokens, .task-block-duration').forEach(el => el.remove());
|
|
949
|
+
const statusBadge = taskBlock.querySelector('.task-block-badge');
|
|
950
|
+
const tokText = formatTokensBadge(usage.totalTokens);
|
|
951
|
+
const durText = formatDuration(usage.durationMs);
|
|
952
|
+
const insertBefore = (el) => { if (statusBadge) statusBadge.before(el); else header.appendChild(el); };
|
|
953
|
+
if (durText) {
|
|
954
|
+
const durEl = document.createElement('span');
|
|
955
|
+
durEl.className = 'task-block-duration';
|
|
956
|
+
durEl.textContent = durText;
|
|
957
|
+
insertBefore(durEl);
|
|
958
|
+
}
|
|
959
|
+
if (tokText) {
|
|
960
|
+
const tokEl = document.createElement('span');
|
|
961
|
+
tokEl.className = 'task-block-tokens';
|
|
962
|
+
tokEl.textContent = tokText;
|
|
963
|
+
insertBefore(tokEl);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Update or create preview
|
|
969
|
+
let preview = taskBlock.querySelector('.task-block-preview');
|
|
970
|
+
if (isCompleted && agentResponse && !isError) {
|
|
971
|
+
const strippedResponse = this._stripMarkdownForPreview(agentResponse);
|
|
972
|
+
const previewText = strippedResponse.slice(0, 150).trim();
|
|
973
|
+
|
|
974
|
+
if (!preview) {
|
|
975
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
976
|
+
preview = document.createElement('div');
|
|
977
|
+
preview.className = 'task-block-preview';
|
|
978
|
+
header.after(preview);
|
|
979
|
+
}
|
|
980
|
+
preview.textContent = previewText + (strippedResponse.length > 150 ? '...' : '');
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Update or create error display
|
|
984
|
+
let errorEl = taskBlock.querySelector('.task-block-error');
|
|
985
|
+
if (isError && msg.toolError) {
|
|
986
|
+
if (!errorEl) {
|
|
987
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
988
|
+
errorEl = document.createElement('div');
|
|
989
|
+
errorEl.className = 'task-block-error';
|
|
990
|
+
// Insert after header (or after preview if it exists)
|
|
991
|
+
const insertAfter = preview || header;
|
|
992
|
+
insertAfter.after(errorEl);
|
|
993
|
+
}
|
|
994
|
+
errorEl.textContent = msg.toolError;
|
|
995
|
+
} else if (errorEl) {
|
|
996
|
+
errorEl.remove();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Update or create full response in body (preserving children)
|
|
1000
|
+
let body = taskBlock.querySelector('.task-block-body');
|
|
1001
|
+
if (!body) {
|
|
1002
|
+
body = document.createElement('div');
|
|
1003
|
+
body.className = 'task-block-body';
|
|
1004
|
+
taskBlock.appendChild(body);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let responseContainer = body.querySelector('.task-block-response');
|
|
1008
|
+
if (isCompleted && agentResponse) {
|
|
1009
|
+
if (!responseContainer) {
|
|
1010
|
+
responseContainer = document.createElement('div');
|
|
1011
|
+
responseContainer.className = 'task-block-response';
|
|
1012
|
+
// Insert at beginning of body (before children)
|
|
1013
|
+
body.insertBefore(responseContainer, body.firstChild);
|
|
1014
|
+
}
|
|
1015
|
+
responseContainer.innerHTML = this._renderMarkdownSafe(agentResponse);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Add expand chevron if there's expandable content and it doesn't exist
|
|
1019
|
+
const hasExpandableContent = agentResponse || isError || body.querySelector('.task-block-children');
|
|
1020
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
1021
|
+
let expandChevron = header?.querySelector('.task-block-expand');
|
|
1022
|
+
if (hasExpandableContent && header && !expandChevron) {
|
|
1023
|
+
const chevron = document.createElement('span');
|
|
1024
|
+
chevron.className = 'task-block-expand';
|
|
1025
|
+
chevron.textContent = '›';
|
|
1026
|
+
header.appendChild(chevron);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Update a Task block with live progress from task_progress events.
|
|
1032
|
+
* Shows current activity, tool count, and duration between header and body.
|
|
1033
|
+
*/
|
|
1034
|
+
updateTaskProgress(taskBlock, progress) {
|
|
1035
|
+
// Don't update completed tasks
|
|
1036
|
+
if (taskBlock.getAttribute('data-complete') === 'true') return;
|
|
1037
|
+
|
|
1038
|
+
let el = taskBlock.querySelector('.task-progress');
|
|
1039
|
+
if (!el) {
|
|
1040
|
+
el = document.createElement('div');
|
|
1041
|
+
el.className = 'task-progress';
|
|
1042
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
1043
|
+
if (header) header.after(el);
|
|
1044
|
+
else return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Tool icon based on last tool name
|
|
1048
|
+
const toolIcons = {
|
|
1049
|
+
Read: 'file', Edit: 'pencil', Write: 'pencil', Bash: 'terminal',
|
|
1050
|
+
Grep: 'search', Glob: 'search', WebFetch: 'globe', WebSearch: 'globe',
|
|
1051
|
+
};
|
|
1052
|
+
const iconName = toolIcons[progress.lastToolName] || 'sparkle';
|
|
1053
|
+
|
|
1054
|
+
const toolsBadge = S.agent_progress.tools_badge.replace('{count}', progress.toolCount);
|
|
1055
|
+
const durBadge = formatDuration(progress.durationMs);
|
|
1056
|
+
|
|
1057
|
+
const stalledClass = progress.stalled ? ' task-progress-stalled' : '';
|
|
1058
|
+
const stalledText = progress.stalled
|
|
1059
|
+
? `<span class="task-progress-stall-warn">${S.agent_progress.stalled.replace('{duration}', this._formatStallDuration(progress.lastUpdate))}</span>`
|
|
1060
|
+
: '';
|
|
1061
|
+
|
|
1062
|
+
el.className = `task-progress${stalledClass}`;
|
|
1063
|
+
el.innerHTML = `<span class="task-progress-pulse"></span>`
|
|
1064
|
+
+ `<span class="task-progress-desc">${escapeHtml(progress.description)}</span>`
|
|
1065
|
+
+ `<span class="task-progress-badge">${toolsBadge}</span>`
|
|
1066
|
+
+ (durBadge ? `<span class="task-progress-badge">${durBadge}</span>` : '')
|
|
1067
|
+
+ stalledText;
|
|
1068
|
+
|
|
1069
|
+
// Also update the header badge to show "Running" with tool count
|
|
1070
|
+
const badge = taskBlock.querySelector('.task-block-badge');
|
|
1071
|
+
if (badge && badge.classList.contains('task-block-badge-pending')) {
|
|
1072
|
+
badge.textContent = progress.stalled ? 'Stalled' : 'Running...';
|
|
1073
|
+
if (progress.stalled) {
|
|
1074
|
+
badge.className = 'task-block-badge task-block-badge-warn';
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Update usage badges in header (live tokens + duration)
|
|
1079
|
+
const header = taskBlock.querySelector('.task-block-header');
|
|
1080
|
+
if (header) {
|
|
1081
|
+
header.querySelectorAll('.task-block-tokens, .task-block-duration').forEach(e => e.remove());
|
|
1082
|
+
const statusBadge = taskBlock.querySelector('.task-block-badge');
|
|
1083
|
+
const insertBefore = (node) => { if (statusBadge) statusBadge.before(node); else header.appendChild(node); };
|
|
1084
|
+
|
|
1085
|
+
if (durBadge) {
|
|
1086
|
+
const durEl = document.createElement('span');
|
|
1087
|
+
durEl.className = 'task-block-duration';
|
|
1088
|
+
durEl.textContent = durBadge;
|
|
1089
|
+
insertBefore(durEl);
|
|
1090
|
+
}
|
|
1091
|
+
const tokText = formatTokensBadge(progress.totalTokens);
|
|
1092
|
+
if (tokText) {
|
|
1093
|
+
const tokEl = document.createElement('span');
|
|
1094
|
+
tokEl.className = 'task-block-tokens';
|
|
1095
|
+
tokEl.textContent = tokText;
|
|
1096
|
+
insertBefore(tokEl);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
_formatStallDuration(lastUpdate) {
|
|
1102
|
+
const elapsed = Math.floor((Date.now() - lastUpdate) / 1000);
|
|
1103
|
+
if (elapsed < 60) return `${elapsed}s`;
|
|
1104
|
+
return `${Math.floor(elapsed / 60)}m`;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Parse Read tool output to extract line numbers and content
|
|
1109
|
+
* Format: " 123→\tcontent" or " 123→content"
|
|
1110
|
+
*/
|
|
1111
|
+
parseReadOutput(output) {
|
|
1112
|
+
const lines = [];
|
|
1113
|
+
let isTruncated = false;
|
|
1114
|
+
let firstLine = null;
|
|
1115
|
+
let lastLine = null;
|
|
1116
|
+
|
|
1117
|
+
if (!output) return { lines, isTruncated, firstLine, lastLine };
|
|
1118
|
+
|
|
1119
|
+
// Check for truncation markers
|
|
1120
|
+
if (output.includes('(truncated') || output.includes('...truncated')) {
|
|
1121
|
+
isTruncated = true;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Parse lines: Claude CLI uses either " NUMBER→CONTENT" (older) or "NUMBER\tCONTENT" (current)
|
|
1125
|
+
const linePattern = /^\s*(\d+)(?:→|\t)(.*)$/gm;
|
|
1126
|
+
let match;
|
|
1127
|
+
while ((match = linePattern.exec(output)) !== null) {
|
|
1128
|
+
const num = parseInt(match[1], 10);
|
|
1129
|
+
const content = match[2].replace(/^\t/, ''); // Remove leading tab (arrow-format may have → then tab)
|
|
1130
|
+
lines.push({ num, content });
|
|
1131
|
+
|
|
1132
|
+
if (firstLine === null || num < firstLine) firstLine = num;
|
|
1133
|
+
if (lastLine === null || num > lastLine) lastLine = num;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return { lines, isTruncated, firstLine, lastLine };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Render a nice todo list for TodoWrite tool
|
|
1141
|
+
*/
|
|
1142
|
+
truncateOutput(text, maxLen = this.maxOutputLength) {
|
|
1143
|
+
// Handle non-string outputs (e.g., image arrays from screenshots)
|
|
1144
|
+
if (Array.isArray(text)) {
|
|
1145
|
+
// Check if it's image content
|
|
1146
|
+
const hasImage = text.some(item => item?.type === 'image');
|
|
1147
|
+
if (hasImage) {
|
|
1148
|
+
return '[Image content]';
|
|
1149
|
+
}
|
|
1150
|
+
// Otherwise stringify
|
|
1151
|
+
text = JSON.stringify(text);
|
|
1152
|
+
} else if (typeof text === 'object' && text !== null) {
|
|
1153
|
+
text = JSON.stringify(text);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const trimmed = (text || '').trim();
|
|
1157
|
+
if (trimmed.length > maxLen) {
|
|
1158
|
+
return trimmed.slice(0, maxLen);
|
|
1159
|
+
}
|
|
1160
|
+
return trimmed;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Update an existing tool result in the DOM
|
|
1165
|
+
* @returns {boolean} true if element was found and updated, false otherwise
|
|
1166
|
+
*/
|
|
1167
|
+
updateToolResult(msg) {
|
|
1168
|
+
const toolBlock = $(`#tool-${msg.id}`);
|
|
1169
|
+
if (!toolBlock) {
|
|
1170
|
+
console.warn(`[ToolRenderer] Tool block not found: #tool-${msg.id} (${msg.toolName})`);
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Detect context for collapse mode (normal/thinking/agent)
|
|
1175
|
+
const isThinking = !!toolBlock.closest('.thinking-tool-card');
|
|
1176
|
+
const isAgent = !!toolBlock.closest('.task-block-children');
|
|
1177
|
+
const context = isAgent ? 'agent' : isThinking ? 'thinking' : 'normal';
|
|
1178
|
+
const toolType = msg?.toolName ? getToolCategory(msg.toolName) : 'execute';
|
|
1179
|
+
const mode = getToolCollapseMode(context, toolType);
|
|
1180
|
+
const expandOpts = { defaultExpanded: mode === 'expanded' };
|
|
1181
|
+
|
|
1182
|
+
// Handle Bash/Shell blocks - re-render completely since structure changes
|
|
1183
|
+
if (toolBlock.classList.contains('bash-block')) {
|
|
1184
|
+
const newHtml = this.renderBashBlock(msg, expandOpts);
|
|
1185
|
+
toolBlock.outerHTML = newHtml;
|
|
1186
|
+
// Start tracking if this is a background task
|
|
1187
|
+
const newEl = $(`#tool-${msg.id}`);
|
|
1188
|
+
if (newEl?.dataset?.taskId) {
|
|
1189
|
+
bgTaskTracker.track(newEl.dataset.taskId, msg.toolInput?.command);
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Handle Read blocks - re-render completely
|
|
1195
|
+
if (toolBlock.classList.contains('read-block')) {
|
|
1196
|
+
const newHtml = this.renderReadBlock(msg, expandOpts);
|
|
1197
|
+
toolBlock.outerHTML = newHtml;
|
|
1198
|
+
const newEl = $(`#tool-${msg.id}`);
|
|
1199
|
+
if (newEl) this.processReadExcalidraw(newEl);
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Handle Grep blocks - re-render completely
|
|
1204
|
+
if (toolBlock.classList.contains('grep-block')) {
|
|
1205
|
+
const newHtml = this.renderGrepBlock(msg, expandOpts);
|
|
1206
|
+
toolBlock.outerHTML = newHtml;
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Handle Glob blocks - re-render completely
|
|
1211
|
+
if (toolBlock.classList.contains('glob-block')) {
|
|
1212
|
+
const newHtml = this.renderGlobBlock(msg, expandOpts);
|
|
1213
|
+
toolBlock.outerHTML = newHtml;
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Handle Write blocks - re-render completely
|
|
1218
|
+
if (toolBlock.classList.contains('write-block')) {
|
|
1219
|
+
const newHtml = this.renderWriteBlock(msg, expandOpts);
|
|
1220
|
+
toolBlock.outerHTML = newHtml;
|
|
1221
|
+
// Process chart/excalidraw rendering after DOM replacement
|
|
1222
|
+
const newEl = $(`#tool-${msg.id}`);
|
|
1223
|
+
if (newEl) { this.processWriteCharts(newEl); this.processWriteExcalidraw(newEl); }
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Handle WebFetch/WebSearch blocks - re-render completely
|
|
1228
|
+
if (toolBlock.classList.contains('webfetch-block')) {
|
|
1229
|
+
const newHtml = this.renderWebFetchBlock(msg, expandOpts);
|
|
1230
|
+
toolBlock.outerHTML = newHtml;
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Handle EnterPlanMode blocks - re-render completely
|
|
1235
|
+
if (toolBlock.classList.contains('plan-mode-block')) {
|
|
1236
|
+
toolBlock.outerHTML = this.renderEnterPlanBlock(msg);
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Handle Task blocks - update in place to preserve children
|
|
1241
|
+
if (toolBlock.classList.contains('task-block')) {
|
|
1242
|
+
this._updateTaskBlockResult(toolBlock, msg);
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Handle Skill blocks - re-render completely
|
|
1247
|
+
if (toolBlock.classList.contains('skill-block')) {
|
|
1248
|
+
toolBlock.outerHTML = this.renderSkillBlock(msg);
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Handle TaskStop blocks - re-render (check before .tob since tob-stop also has .tob)
|
|
1253
|
+
if (toolBlock.classList.contains('tob-stop')) {
|
|
1254
|
+
toolBlock.outerHTML = this.renderTaskStopBlock(msg);
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Handle TaskOutput blocks - re-render completely
|
|
1259
|
+
if (toolBlock.classList.contains('tob')) {
|
|
1260
|
+
toolBlock.outerHTML = this.renderTaskOutputBlock(msg);
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Handle Edit tool diff blocks - re-render completely for correct line numbers
|
|
1265
|
+
if (toolBlock.classList.contains('edit-tool-block')) {
|
|
1266
|
+
const diffHtml = this.renderEditDiff(msg.toolInput, msg.toolOutput, msg.toolId, msg.startLine);
|
|
1267
|
+
if (diffHtml) {
|
|
1268
|
+
const statusClass = msg.toolError ? 'edit-status-error' : 'edit-status-success';
|
|
1269
|
+
const statusText = msg.toolError ? 'Failed: ' + escapeHtml(msg.toolError) : 'Applied';
|
|
1270
|
+
toolBlock.innerHTML = `${diffHtml}\n<div class="edit-status ${statusClass}">${statusText}</div>`;
|
|
1271
|
+
} else if (msg.toolError) {
|
|
1272
|
+
const editStatus = toolBlock.querySelector('.edit-status');
|
|
1273
|
+
if (editStatus) {
|
|
1274
|
+
editStatus.className = 'edit-status edit-status-error';
|
|
1275
|
+
editStatus.textContent = 'Failed: ' + msg.toolError;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Handle regular tool blocks
|
|
1282
|
+
const outputEl = toolBlock.querySelector('.tool-output');
|
|
1283
|
+
if (outputEl) {
|
|
1284
|
+
outputEl.className = `tool-output ${msg.toolError ? 'error' : ''}`;
|
|
1285
|
+
outputEl.textContent = this.truncateOutput(msg.toolOutput || msg.toolError || S.tool_renderer.badges.done);
|
|
1286
|
+
}
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Process chart rendering in Write tool blocks
|
|
1292
|
+
* Finds .write-chart-pending elements, fetches SVG from API, replaces loading state
|
|
1293
|
+
*/
|
|
1294
|
+
processWriteCharts(container) {
|
|
1295
|
+
if (!container) return;
|
|
1296
|
+
const pending = container.querySelectorAll('.write-chart-pending');
|
|
1297
|
+
if (pending.length === 0) return;
|
|
1298
|
+
|
|
1299
|
+
pending.forEach(el => {
|
|
1300
|
+
el.classList.remove('write-chart-pending');
|
|
1301
|
+
|
|
1302
|
+
const encoded = el.dataset.chartJson;
|
|
1303
|
+
if (!encoded) return;
|
|
1304
|
+
|
|
1305
|
+
let jsonText;
|
|
1306
|
+
try {
|
|
1307
|
+
jsonText = decodeURIComponent(escape(atob(encoded)));
|
|
1308
|
+
} catch {
|
|
1309
|
+
el.innerHTML = `<div class="chart-inline-error">${S.tool_renderer.errors.decode_chart}</div>`;
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
|
|
1314
|
+
el.innerHTML = `<div class="chart-inline-error">${S.tool_renderer.errors.chart_disabled}</div>`;
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
fetch('/api/chart/render', {
|
|
1319
|
+
method: 'POST',
|
|
1320
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1321
|
+
body: jsonText,
|
|
1322
|
+
})
|
|
1323
|
+
.then(resp => {
|
|
1324
|
+
if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
|
|
1325
|
+
return resp.text();
|
|
1326
|
+
})
|
|
1327
|
+
.then(svg => {
|
|
1328
|
+
el.innerHTML = `<div class="chart-inline-rendered"><div class="chart-inline-svg">${sanitizeSvg(svg)}</div></div>`;
|
|
1329
|
+
})
|
|
1330
|
+
.catch(err => {
|
|
1331
|
+
console.error('Write chart render error:', err);
|
|
1332
|
+
el.innerHTML = `<div class="chart-inline-error"><span>Chart render error: ${escapeHtml(err.message)}</span></div>`;
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Post-DOM rendering for excalidraw Write blocks — sends JSON to /api/excalidraw/render
|
|
1339
|
+
*/
|
|
1340
|
+
processWriteExcalidraw(container) {
|
|
1341
|
+
if (!container) return;
|
|
1342
|
+
const pending = container.querySelectorAll('.write-excalidraw-pending');
|
|
1343
|
+
if (pending.length === 0) return;
|
|
1344
|
+
|
|
1345
|
+
pending.forEach(el => {
|
|
1346
|
+
el.classList.remove('write-excalidraw-pending');
|
|
1347
|
+
|
|
1348
|
+
const encoded = el.dataset.excalidrawJson;
|
|
1349
|
+
if (!encoded) return;
|
|
1350
|
+
|
|
1351
|
+
let jsonText;
|
|
1352
|
+
try {
|
|
1353
|
+
jsonText = decodeURIComponent(escape(atob(encoded)));
|
|
1354
|
+
} catch {
|
|
1355
|
+
el.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.decode_diagram}</div>`;
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
|
|
1360
|
+
el.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.diagram_disabled}</div>`;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
fetch('/api/excalidraw/render?dark=true', {
|
|
1365
|
+
method: 'POST',
|
|
1366
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1367
|
+
body: jsonText,
|
|
1368
|
+
})
|
|
1369
|
+
.then(resp => {
|
|
1370
|
+
if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
|
|
1371
|
+
return resp.text();
|
|
1372
|
+
})
|
|
1373
|
+
.then(svg => {
|
|
1374
|
+
const fp = el.closest('.write-excalidraw')?.dataset?.filePath || '';
|
|
1375
|
+
el.innerHTML = `<div class="excalidraw-inline-rendered"><div class="excalidraw-inline-svg excalidraw-clickable" data-act="preview-file" data-file="${escapeAttr(fp)}">${sanitizeSvg(svg)}</div></div>`;
|
|
1376
|
+
})
|
|
1377
|
+
.catch(err => {
|
|
1378
|
+
console.error('Write excalidraw render error:', err);
|
|
1379
|
+
el.innerHTML = `<div class="excalidraw-inline-error"><span>Diagram render error: ${escapeHtml(err.message)}</span></div>`;
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Post-DOM rendering for excalidraw Read blocks — fetches SVG from /api/file-raw
|
|
1386
|
+
*/
|
|
1387
|
+
processReadExcalidraw(container) {
|
|
1388
|
+
if (!container) return;
|
|
1389
|
+
const targets = container.querySelectorAll('.read-excalidraw-loading');
|
|
1390
|
+
if (targets.length === 0) return;
|
|
1391
|
+
|
|
1392
|
+
targets.forEach(loading => {
|
|
1393
|
+
const svgDiv = loading.nextElementSibling;
|
|
1394
|
+
if (!svgDiv?.dataset?.src) return;
|
|
1395
|
+
|
|
1396
|
+
if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
|
|
1397
|
+
loading.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.diagram_disabled}</div>`;
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
fetch(svgDiv.dataset.src)
|
|
1402
|
+
.then(resp => {
|
|
1403
|
+
if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
|
|
1404
|
+
return resp.text();
|
|
1405
|
+
})
|
|
1406
|
+
.then(svg => {
|
|
1407
|
+
const fp = svgDiv.closest('.read-excalidraw')?.dataset?.filePath || '';
|
|
1408
|
+
svgDiv.innerHTML = `<div class="excalidraw-inline-svg excalidraw-clickable" data-act="preview-file" data-file="${escapeAttr(fp)}">${sanitizeSvg(svg)}</div>`;
|
|
1409
|
+
svgDiv.style.display = '';
|
|
1410
|
+
loading.remove();
|
|
1411
|
+
})
|
|
1412
|
+
.catch(err => {
|
|
1413
|
+
console.error('Read excalidraw render error:', err);
|
|
1414
|
+
loading.innerHTML = `<div class="excalidraw-inline-error"><span>Diagram render error: ${escapeHtml(err.message)}</span></div>`;
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Toggle tool block collapsed state
|
|
1421
|
+
*/
|
|
1422
|
+
toggleTool(msgId) {
|
|
1423
|
+
const toolBlock = $(`#tool-${msgId}`)?.querySelector('.tool-block') || $(`#tool-${msgId}`);
|
|
1424
|
+
if (toolBlock) {
|
|
1425
|
+
toolBlock.classList.toggle('collapsed');
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Render tool output with expansion handling
|
|
1431
|
+
*/
|
|
1432
|
+
renderToolOutput(content, isCompleted = false, threshold = 5) {
|
|
1433
|
+
if (!content) return isCompleted ? S.tool_renderer.badges.done : '';
|
|
1434
|
+
|
|
1435
|
+
const lines = content.split('\n');
|
|
1436
|
+
if (lines.length <= threshold) {
|
|
1437
|
+
return escapeHtml(content);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Show first few lines with expand option
|
|
1441
|
+
const preview = lines.slice(0, threshold).join('\n');
|
|
1442
|
+
return `<div class="tool-output-expandable">
|
|
1443
|
+
<div class="tool-output-preview">${escapeHtml(preview)}</div>
|
|
1444
|
+
<div class="tool-output-full hidden">${escapeHtml(content)}</div>
|
|
1445
|
+
<button class="tool-output-expand" data-act="toggle-tool-output" data-block=".tool-output-expandable">Show more</button>
|
|
1446
|
+
</div>`;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Render a compact styled tool block for thinking messages.
|
|
1451
|
+
* Each tool type gets a distinct visual treatment.
|
|
1452
|
+
* @param {Object} tool - Tool object with toolName, toolInput, toolOutput, toolId, toolCompleted
|
|
1453
|
+
* @returns {string} HTML for the thinking tool block
|
|
1454
|
+
*/
|
|
1455
|
+
_parseBashResult(cmd, output) {
|
|
1456
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
1457
|
+
|
|
1458
|
+
// ls command - show file count or file names
|
|
1459
|
+
if (cmd.match(/^ls\b/)) {
|
|
1460
|
+
if (lines.length <= 3) {
|
|
1461
|
+
// Few files - show names
|
|
1462
|
+
const names = lines.map(l => l.split(/\s+/).pop()).filter(Boolean);
|
|
1463
|
+
return `<span class="ta-result">${names.join(', ')}</span>`;
|
|
1464
|
+
}
|
|
1465
|
+
return `<span class="ta-result">${lines.length} items</span>`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// git status - summarize
|
|
1469
|
+
if (cmd.match(/^git\s+status/)) {
|
|
1470
|
+
const modified = (output.match(/modified:/g) || []).length;
|
|
1471
|
+
const added = (output.match(/new file:/g) || []).length;
|
|
1472
|
+
const deleted = (output.match(/deleted:/g) || []).length;
|
|
1473
|
+
if (modified + added + deleted === 0) {
|
|
1474
|
+
return '<span class="ta-muted">clean</span>';
|
|
1475
|
+
}
|
|
1476
|
+
const parts = [];
|
|
1477
|
+
if (modified) parts.push(`${modified} modified`);
|
|
1478
|
+
if (added) parts.push(`${added} new`);
|
|
1479
|
+
if (deleted) parts.push(`${deleted} deleted`);
|
|
1480
|
+
return `<span class="ta-result">${parts.join(', ')}</span>`;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// git diff --stat
|
|
1484
|
+
if (cmd.match(/^git\s+diff/)) {
|
|
1485
|
+
const statMatch = output.match(/(\d+) files? changed/);
|
|
1486
|
+
if (statMatch) {
|
|
1487
|
+
return `<span class="ta-result">${statMatch[0]}</span>`;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Default: line count
|
|
1492
|
+
return `<span class="ta-result">${lines.length} line${lines.length !== 1 ? 's' : ''}</span>`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/** Bash/Shell - terminal style with command and output preview */
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
// Mix block-mode and thinking-mode methods into ToolRenderer.prototype.
|
|
1500
|
+
Object.assign(ToolRenderer.prototype, blockMethods, thinkingMethods);
|