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,1057 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Explorer Widget - Session log viewer using the widget system
|
|
3
|
+
*
|
|
4
|
+
* Provides a bottom-sheet/sidebar panel for viewing session logs.
|
|
5
|
+
* Migrated from log-explorer.js to use the modular widget system.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Four tabs: Messages, Raw, Tools, System
|
|
9
|
+
* - Pagination with "Load More"
|
|
10
|
+
* - Filtering (role, errors only)
|
|
11
|
+
* - Sort order toggle
|
|
12
|
+
* - JSON tree rendering with expand/collapse
|
|
13
|
+
* - Transform to sidebar for desktop use
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import S from '../strings.js';
|
|
17
|
+
import { CONFIG } from '../config.js';
|
|
18
|
+
import { escapeHtml, formatSize, highlightThinkingKeywords, formatTimePrecise } from '../utils.js';
|
|
19
|
+
import { WidgetManager, WidgetBus, ICONS } from '../widget-system/index.js';
|
|
20
|
+
import { isThinkingKeywordsHighlightingEnabled } from './config-widget.js';
|
|
21
|
+
import { renderJsonTree } from '../preview/json-tree.js';
|
|
22
|
+
import { copyToClipboard, showToast } from '../context-menu.js';
|
|
23
|
+
import { engineAuthorLabel } from '../status-bar.js';
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Icons (specific to log explorer)
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const LOG_ICONS = {
|
|
30
|
+
logs: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
|
31
|
+
user: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
|
|
32
|
+
assistant: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
|
33
|
+
tool: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
|
|
34
|
+
result: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>',
|
|
35
|
+
thinking: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v.01"/><path d="M12 8a2 2 0 0 1 2 2c0 .74-.4 1.38-1 1.73V13a1 1 0 0 1-2 0v-1.27c-.6-.35-1-.99-1-1.73a2 2 0 0 1 2-2z"/></svg>',
|
|
36
|
+
error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
|
|
37
|
+
event: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
|
|
38
|
+
input: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>',
|
|
39
|
+
output: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>',
|
|
40
|
+
chevron: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>',
|
|
41
|
+
external: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>',
|
|
42
|
+
refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
|
|
43
|
+
sortDesc: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 4h13M3 8h9M3 12h5M17 8v12M17 20l-4-4M17 20l4-4"/></svg>',
|
|
44
|
+
sortAsc: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h13M3 16h9M3 20h5M17 4v12M17 4l-4 4M17 4l4 4"/></svg>',
|
|
45
|
+
copy: '<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>',
|
|
46
|
+
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Role colors and icons
|
|
50
|
+
const ROLE_CONFIG = {
|
|
51
|
+
user: { icon: LOG_ICONS.user, color: '#3b82f6', label: S.widgets.log_explorer.message_types.user },
|
|
52
|
+
assistant: { icon: LOG_ICONS.assistant, color: '#22c55e', label: S.widgets.log_explorer.message_types.assistant },
|
|
53
|
+
tool: { icon: LOG_ICONS.tool, color: '#f59e0b', label: S.widgets.log_explorer.message_types.tool },
|
|
54
|
+
thinking: { icon: LOG_ICONS.thinking, color: '#a855f7', label: S.widgets.log_explorer.message_types.thinking },
|
|
55
|
+
result: { icon: LOG_ICONS.result, color: '#64748b', label: S.widgets.log_explorer.message_types.result },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Raw direction config
|
|
59
|
+
const DIR_CONFIG = {
|
|
60
|
+
in: { icon: LOG_ICONS.input, color: '#3b82f6', label: 'Input' },
|
|
61
|
+
out: { icon: LOG_ICONS.output, color: '#22c55e', label: 'Output' },
|
|
62
|
+
event: { icon: LOG_ICONS.event, color: '#64748b', label: 'Event' },
|
|
63
|
+
error: { icon: LOG_ICONS.error, color: '#ef4444', label: 'Error' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
67
|
+
// State Management
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
class LogExplorerState {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.currentTab = 'messages'; // 'messages', 'raw', 'tools', 'system'
|
|
73
|
+
this.sessionId = null;
|
|
74
|
+
this.sessionMeta = null;
|
|
75
|
+
|
|
76
|
+
// Pagination
|
|
77
|
+
this.messagesOffset = 0;
|
|
78
|
+
this.rawOffset = 0;
|
|
79
|
+
this.hasMoreMessages = false;
|
|
80
|
+
this.hasMoreRaw = false;
|
|
81
|
+
|
|
82
|
+
// Filters
|
|
83
|
+
this.roleFilter = null;
|
|
84
|
+
this.dirFilter = null;
|
|
85
|
+
this.errorsOnly = false;
|
|
86
|
+
|
|
87
|
+
// Sort order: 'desc' (newest first) or 'asc' (oldest first)
|
|
88
|
+
this.sortOrder = 'desc';
|
|
89
|
+
|
|
90
|
+
// DOM references (updated on render)
|
|
91
|
+
this.container = null;
|
|
92
|
+
this.tabsEl = null;
|
|
93
|
+
this.contentEl = null;
|
|
94
|
+
this.loadingEl = null;
|
|
95
|
+
this.emptyEl = null;
|
|
96
|
+
this.errorEl = null;
|
|
97
|
+
this.loadMoreBtn = null;
|
|
98
|
+
this.sessionInfoEl = null;
|
|
99
|
+
this.sortBtn = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
reset() {
|
|
103
|
+
this.currentTab = 'messages';
|
|
104
|
+
this.sessionMeta = null;
|
|
105
|
+
this.messagesOffset = 0;
|
|
106
|
+
this.rawOffset = 0;
|
|
107
|
+
this.hasMoreMessages = false;
|
|
108
|
+
this.hasMoreRaw = false;
|
|
109
|
+
this.roleFilter = null;
|
|
110
|
+
this.dirFilter = null;
|
|
111
|
+
this.errorsOnly = false;
|
|
112
|
+
this.sortOrder = 'desc';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Per-session state map
|
|
117
|
+
const states = new Map();
|
|
118
|
+
|
|
119
|
+
function getState(sessionId) {
|
|
120
|
+
if (!sessionId) sessionId = WidgetManager.currentSessionId;
|
|
121
|
+
if (!states.has(sessionId)) states.set(sessionId, new LogExplorerState());
|
|
122
|
+
return states.get(sessionId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function destroyState(sessionId) {
|
|
126
|
+
states.delete(sessionId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Resolve a session identifier to its server-side storeId. ctx.sessionId may
|
|
130
|
+
// be the client `sess_xxx` ID (assigned at Session construction) for a fresh
|
|
131
|
+
// session that hasn't yet received a server-assigned storeId — but the logs
|
|
132
|
+
// API endpoints key on storeId. Returns null if the session has no storeId
|
|
133
|
+
// yet (fresh, no message sent), meaning there are no server-side logs.
|
|
134
|
+
function resolveStoreId(maybeId) {
|
|
135
|
+
if (!maybeId) return null;
|
|
136
|
+
const sessions = window.app?.sessionManager?.sessions || [];
|
|
137
|
+
const session = sessions.find(s => s.id === maybeId || s.storeId === maybeId);
|
|
138
|
+
if (session) return session.storeId || null;
|
|
139
|
+
// Unknown to the session manager — could be a historical session ID
|
|
140
|
+
// opened directly. Client IDs are sess_-prefixed; anything else is treated
|
|
141
|
+
// as already a storeId.
|
|
142
|
+
if (maybeId.startsWith('sess_')) return null;
|
|
143
|
+
return maybeId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
147
|
+
// Widget Registration
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
export function registerLogExplorerWidget() {
|
|
151
|
+
WidgetManager.register('log-explorer', {
|
|
152
|
+
id: 'log-explorer',
|
|
153
|
+
type: 'bottom-sheet',
|
|
154
|
+
title: S.widgets.titles.logs,
|
|
155
|
+
icon: LOG_ICONS.logs,
|
|
156
|
+
shortcut: 'Alt+L',
|
|
157
|
+
|
|
158
|
+
heights: { half: '45vh', full: '85vh' },
|
|
159
|
+
sessionAware: true,
|
|
160
|
+
|
|
161
|
+
render: renderLogExplorer,
|
|
162
|
+
onOpen: handleOpen,
|
|
163
|
+
onClose: handleClose,
|
|
164
|
+
|
|
165
|
+
headerActions: [
|
|
166
|
+
{
|
|
167
|
+
id: 'sort',
|
|
168
|
+
icon: LOG_ICONS.sortDesc,
|
|
169
|
+
title: S.widgets.header_actions.sort_order,
|
|
170
|
+
onClick: toggleSortOrder
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'refresh',
|
|
174
|
+
icon: LOG_ICONS.refresh,
|
|
175
|
+
title: S.widgets.header_actions.refresh,
|
|
176
|
+
onClick: refresh
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'external',
|
|
180
|
+
icon: LOG_ICONS.external,
|
|
181
|
+
title: S.widgets.header_actions.open_viewer,
|
|
182
|
+
onClick: openExternal
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
onDestroy: (sessionId) => {
|
|
187
|
+
destroyState(sessionId);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
193
|
+
// Lifecycle Handlers
|
|
194
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function handleOpen() {
|
|
197
|
+
const state = getState();
|
|
198
|
+
// Always re-resolve: storeId may have been assigned since last open
|
|
199
|
+
// (fresh session that has now sent its first message).
|
|
200
|
+
const sessionKey = state.sessionId || WidgetManager.currentSessionId;
|
|
201
|
+
state.sessionId = resolveStoreId(sessionKey);
|
|
202
|
+
if (state.sessionId) {
|
|
203
|
+
loadOverview();
|
|
204
|
+
} else {
|
|
205
|
+
showEmpty(S.widgets.log_explorer.no_logs_yet);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleClose() {
|
|
210
|
+
// Nothing special needed on close
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
214
|
+
// Main Render Function
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function renderLogExplorer(container, ctx) {
|
|
218
|
+
const state = getState(ctx.sessionId);
|
|
219
|
+
state.container = container;
|
|
220
|
+
if (ctx.sessionId) state.sessionId = resolveStoreId(ctx.sessionId);
|
|
221
|
+
|
|
222
|
+
container.innerHTML = `
|
|
223
|
+
<div class="log-explorer-inner">
|
|
224
|
+
<div class="logs-header">
|
|
225
|
+
<div class="logs-session-info">
|
|
226
|
+
<span class="logs-session-name">Loading...</span>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="logs-tabs">
|
|
230
|
+
<button class="logs-tab active" data-tab="messages">Messages</button>
|
|
231
|
+
<button class="logs-tab" data-tab="raw">Raw</button>
|
|
232
|
+
<button class="logs-tab" data-tab="tools">Tools</button>
|
|
233
|
+
<button class="logs-tab" data-tab="system">System</button>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="logs-body">
|
|
236
|
+
<div class="logs-loading" hidden>Loading...</div>
|
|
237
|
+
<div class="logs-empty" hidden>No data</div>
|
|
238
|
+
<div class="logs-error" hidden></div>
|
|
239
|
+
<div class="logs-content"></div>
|
|
240
|
+
<button class="logs-load-more" hidden>Load more</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
// Cache DOM references
|
|
246
|
+
state.sessionInfoEl = container.querySelector('.logs-session-info');
|
|
247
|
+
state.tabsEl = container.querySelector('.logs-tabs');
|
|
248
|
+
state.contentEl = container.querySelector('.logs-content');
|
|
249
|
+
state.loadingEl = container.querySelector('.logs-loading');
|
|
250
|
+
state.emptyEl = container.querySelector('.logs-empty');
|
|
251
|
+
state.errorEl = container.querySelector('.logs-error');
|
|
252
|
+
state.loadMoreBtn = container.querySelector('.logs-load-more');
|
|
253
|
+
|
|
254
|
+
// Event listeners
|
|
255
|
+
setupEventListeners();
|
|
256
|
+
|
|
257
|
+
// Load data if we have a session
|
|
258
|
+
if (state.sessionId) {
|
|
259
|
+
loadOverview();
|
|
260
|
+
} else if (ctx.sessionId) {
|
|
261
|
+
// Session exists but has no storeId yet (fresh, no message sent)
|
|
262
|
+
showEmpty(S.widgets.log_explorer.no_logs_yet);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function setupEventListeners() {
|
|
267
|
+
const state = getState();
|
|
268
|
+
// Tab switching
|
|
269
|
+
state.tabsEl?.addEventListener('click', (e) => {
|
|
270
|
+
const tab = e.target.closest('[data-tab]');
|
|
271
|
+
if (tab) {
|
|
272
|
+
switchTab(tab.dataset.tab);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Load more
|
|
277
|
+
state.loadMoreBtn?.addEventListener('click', loadMore);
|
|
278
|
+
|
|
279
|
+
// Delegated click handlers for content
|
|
280
|
+
state.contentEl?.addEventListener('click', handleContentClick);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleContentClick(e) {
|
|
284
|
+
const state = getState();
|
|
285
|
+
|
|
286
|
+
// Copy a raw entry to the clipboard (sits inside the header, so intercept
|
|
287
|
+
// before the expand/collapse handler swallows the click).
|
|
288
|
+
const copyBtn = e.target.closest('.log-raw-copy');
|
|
289
|
+
if (copyBtn) {
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
let text = '';
|
|
293
|
+
try {
|
|
294
|
+
text = decodeURIComponent(escape(atob(copyBtn.dataset.copy || '')));
|
|
295
|
+
} catch { text = ''; }
|
|
296
|
+
copyToClipboard(text).then(ok => {
|
|
297
|
+
if (ok) {
|
|
298
|
+
copyBtn.classList.add('copied');
|
|
299
|
+
copyBtn.innerHTML = LOG_ICONS.check;
|
|
300
|
+
showToast(S.toast.copied);
|
|
301
|
+
setTimeout(() => {
|
|
302
|
+
copyBtn.classList.remove('copied');
|
|
303
|
+
copyBtn.innerHTML = LOG_ICONS.copy;
|
|
304
|
+
}, 1200);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Log item expand/collapse
|
|
311
|
+
const expandable = e.target.closest('.log-item-expandable');
|
|
312
|
+
if (expandable && !e.target.closest('.json-toggle') && !e.target.closest('.json-parse-btn')) {
|
|
313
|
+
if (e.target.closest('.log-item-header')) {
|
|
314
|
+
expandable.classList.toggle('expanded');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// JSON tree node toggle
|
|
319
|
+
const jsonToggle = e.target.closest('.json-toggle');
|
|
320
|
+
if (jsonToggle) {
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
const collapsible = jsonToggle.closest('.json-collapsible');
|
|
323
|
+
if (collapsible) {
|
|
324
|
+
collapsible.classList.toggle('expanded');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Nested JSON parse button
|
|
329
|
+
const parseBtn = e.target.closest('.json-parse-btn');
|
|
330
|
+
if (parseBtn) {
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
const container = parseBtn.closest('.json-nested-container');
|
|
333
|
+
if (container) {
|
|
334
|
+
const nestedContent = container.querySelector('.json-nested-content');
|
|
335
|
+
const preview = container.querySelector('.json-string-preview');
|
|
336
|
+
if (nestedContent) {
|
|
337
|
+
const isHidden = nestedContent.hidden;
|
|
338
|
+
nestedContent.hidden = !isHidden;
|
|
339
|
+
parseBtn.classList.toggle('active', isHidden);
|
|
340
|
+
if (preview) preview.hidden = isHidden;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Tool file link
|
|
346
|
+
const toolLink = e.target.closest('[data-tool-file]');
|
|
347
|
+
if (toolLink) {
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
showToolOutput(toolLink.dataset.toolFile);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Filter button
|
|
353
|
+
const filterBtn = e.target.closest('[data-filter]');
|
|
354
|
+
if (filterBtn) {
|
|
355
|
+
const filter = filterBtn.dataset.filter;
|
|
356
|
+
if (filter === 'errors') {
|
|
357
|
+
state.errorsOnly = !state.errorsOnly;
|
|
358
|
+
loadRaw(false);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
364
|
+
// Data Loading
|
|
365
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
async function loadOverview() {
|
|
368
|
+
const state = getState();
|
|
369
|
+
if (!state.sessionId) return;
|
|
370
|
+
|
|
371
|
+
showLoading();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs`);
|
|
375
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
376
|
+
|
|
377
|
+
const data = await response.json();
|
|
378
|
+
state.sessionMeta = data.meta;
|
|
379
|
+
updateSessionInfo(data);
|
|
380
|
+
updateTabCounts(data.files);
|
|
381
|
+
|
|
382
|
+
await loadCurrentTab();
|
|
383
|
+
} catch (err) {
|
|
384
|
+
showError(`Failed to load logs: ${err.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function loadCurrentTab() {
|
|
389
|
+
const state = getState();
|
|
390
|
+
switch (state.currentTab) {
|
|
391
|
+
case 'messages':
|
|
392
|
+
await loadMessages(false);
|
|
393
|
+
break;
|
|
394
|
+
case 'raw':
|
|
395
|
+
await loadRaw(false);
|
|
396
|
+
break;
|
|
397
|
+
case 'tools':
|
|
398
|
+
await loadTools();
|
|
399
|
+
break;
|
|
400
|
+
case 'system':
|
|
401
|
+
renderSystemLogs();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function loadMessages(append = false) {
|
|
407
|
+
const state = getState();
|
|
408
|
+
if (!state.sessionId) return;
|
|
409
|
+
if (!append) {
|
|
410
|
+
state.messagesOffset = 0;
|
|
411
|
+
showLoading();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
let url = `${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/messages?offset=${state.messagesOffset}&limit=50`;
|
|
416
|
+
url += `&sort=${state.sortOrder}`;
|
|
417
|
+
if (state.roleFilter) {
|
|
418
|
+
url += `&role=${state.roleFilter}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const response = await fetch(url);
|
|
422
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
423
|
+
|
|
424
|
+
const data = await response.json();
|
|
425
|
+
state.hasMoreMessages = data.has_more;
|
|
426
|
+
state.messagesOffset = data.offset + data.messages.length;
|
|
427
|
+
|
|
428
|
+
if (append) {
|
|
429
|
+
appendMessages(data.messages);
|
|
430
|
+
} else {
|
|
431
|
+
renderMessages(data.messages, data.total);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
updateLoadMore(state.hasMoreMessages);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
showError(`Failed to load messages: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function loadRaw(append = false) {
|
|
441
|
+
const state = getState();
|
|
442
|
+
if (!state.sessionId) return;
|
|
443
|
+
if (!append) {
|
|
444
|
+
state.rawOffset = 0;
|
|
445
|
+
showLoading();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
let url = `${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/raw?offset=${state.rawOffset}&limit=100`;
|
|
450
|
+
url += `&sort=${state.sortOrder}`;
|
|
451
|
+
if (state.dirFilter) {
|
|
452
|
+
url += `&direction=${state.dirFilter}`;
|
|
453
|
+
}
|
|
454
|
+
if (state.errorsOnly) {
|
|
455
|
+
url += `&errors_only=true`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const response = await fetch(url);
|
|
459
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
460
|
+
|
|
461
|
+
const data = await response.json();
|
|
462
|
+
state.hasMoreRaw = data.has_more;
|
|
463
|
+
state.rawOffset = data.offset + data.entries.length;
|
|
464
|
+
|
|
465
|
+
if (append) {
|
|
466
|
+
appendRawEntries(data.entries);
|
|
467
|
+
} else {
|
|
468
|
+
renderRaw(data.entries, data.total);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
updateLoadMore(state.hasMoreRaw);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
showError(`Failed to load raw log: ${err.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function loadTools() {
|
|
478
|
+
const state = getState();
|
|
479
|
+
if (!state.sessionId) return;
|
|
480
|
+
showLoading();
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/tools`);
|
|
484
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
485
|
+
|
|
486
|
+
const data = await response.json();
|
|
487
|
+
renderTools(data.files);
|
|
488
|
+
updateLoadMore(false);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
showError(`Failed to load tools: ${err.message}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function showToolOutput(filename) {
|
|
495
|
+
const state = getState();
|
|
496
|
+
if (!state.sessionId) return;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/sessions/${state.sessionId}/logs/tools/${encodeURIComponent(filename)}`);
|
|
500
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
501
|
+
|
|
502
|
+
const data = await response.json();
|
|
503
|
+
const content = escapeHtml(data.content);
|
|
504
|
+
|
|
505
|
+
state.contentEl.innerHTML = `
|
|
506
|
+
<div class="log-tool-preview">
|
|
507
|
+
<div class="log-tool-preview-header">
|
|
508
|
+
<button class="log-tool-preview-back" data-tooltip="Back to list">
|
|
509
|
+
${LOG_ICONS.input}
|
|
510
|
+
</button>
|
|
511
|
+
<span class="log-tool-preview-name">${escapeHtml(filename)}</span>
|
|
512
|
+
<span class="log-tool-preview-size">${formatSize(data.size)}</span>
|
|
513
|
+
</div>
|
|
514
|
+
<pre class="log-tool-preview-content"><code>${content}</code></pre>
|
|
515
|
+
</div>
|
|
516
|
+
`;
|
|
517
|
+
|
|
518
|
+
state.contentEl.querySelector('.log-tool-preview-back')?.addEventListener('click', () => {
|
|
519
|
+
loadTools();
|
|
520
|
+
});
|
|
521
|
+
} catch (err) {
|
|
522
|
+
showError(`Failed to load tool output: ${err.message}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Render system logs from the active session (client-side only)
|
|
528
|
+
* System logs are stored in-memory per session (connection events, errors, etc.)
|
|
529
|
+
*/
|
|
530
|
+
function renderSystemLogs() {
|
|
531
|
+
const state = getState();
|
|
532
|
+
// Get system logs from the session matching our state.sessionId
|
|
533
|
+
const session = window.app?.sessionManager?.sessions?.find(s => s.storeId === state.sessionId);
|
|
534
|
+
const logs = session?.systemLogs || [];
|
|
535
|
+
|
|
536
|
+
if (logs.length === 0) {
|
|
537
|
+
showEmpty('No system events');
|
|
538
|
+
updateLoadMore(false);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Sort according to current sort order
|
|
543
|
+
const sortedLogs = [...logs].sort((a, b) => {
|
|
544
|
+
const aTime = new Date(a.timestamp).getTime();
|
|
545
|
+
const bTime = new Date(b.timestamp).getTime();
|
|
546
|
+
return state.sortOrder === 'desc' ? bTime - aTime : aTime - bTime;
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const html = sortedLogs.map(entry => {
|
|
550
|
+
const time = formatTimePrecise(entry.timestamp);
|
|
551
|
+
const typeClass = entry.type || 'info';
|
|
552
|
+
const typeIcon = typeClass === 'error' ? LOG_ICONS.error :
|
|
553
|
+
typeClass === 'warning' ? LOG_ICONS.event :
|
|
554
|
+
LOG_ICONS.event;
|
|
555
|
+
|
|
556
|
+
return `
|
|
557
|
+
<div class="log-item log-system-item ${typeClass}">
|
|
558
|
+
<div class="log-item-header">
|
|
559
|
+
<span class="log-badge ${typeClass}">
|
|
560
|
+
${typeIcon}
|
|
561
|
+
<span class="log-badge-label">${typeClass}</span>
|
|
562
|
+
</span>
|
|
563
|
+
<span class="log-time">${time}</span>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="log-system-text">${escapeHtml(entry.text)}</div>
|
|
566
|
+
</div>
|
|
567
|
+
`;
|
|
568
|
+
}).join('');
|
|
569
|
+
|
|
570
|
+
state.contentEl.innerHTML = html;
|
|
571
|
+
state.loadingEl.hidden = true;
|
|
572
|
+
state.emptyEl.hidden = true;
|
|
573
|
+
updateLoadMore(false);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function loadMore() {
|
|
577
|
+
const state = getState();
|
|
578
|
+
if (state.currentTab === 'messages' && state.hasMoreMessages) {
|
|
579
|
+
loadMessages(true);
|
|
580
|
+
} else if (state.currentTab === 'raw' && state.hasMoreRaw) {
|
|
581
|
+
loadRaw(true);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function refresh() {
|
|
586
|
+
const state = getState();
|
|
587
|
+
// Re-resolve in case storeId was assigned since the widget opened
|
|
588
|
+
// (fresh session that has now sent its first message).
|
|
589
|
+
const sessionKey = state.sessionId || WidgetManager.currentSessionId;
|
|
590
|
+
state.sessionId = resolveStoreId(sessionKey);
|
|
591
|
+
if (state.sessionId) {
|
|
592
|
+
loadOverview();
|
|
593
|
+
} else {
|
|
594
|
+
showEmpty(S.widgets.log_explorer.no_logs_yet);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
599
|
+
// UI Updates
|
|
600
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
function updateSessionInfo(data) {
|
|
603
|
+
const state = getState();
|
|
604
|
+
if (!state.sessionInfoEl) return;
|
|
605
|
+
|
|
606
|
+
const meta = data.meta || {};
|
|
607
|
+
const cost = meta.total_cost ? `$${meta.total_cost.toFixed(4)}` : '';
|
|
608
|
+
const name = meta.name || 'Session';
|
|
609
|
+
|
|
610
|
+
state.sessionInfoEl.innerHTML = `
|
|
611
|
+
<span class="logs-session-name">${escapeHtml(name)}</span>
|
|
612
|
+
${cost ? `<span class="logs-session-cost">${cost}</span>` : ''}
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function updateTabCounts(files) {
|
|
617
|
+
const state = getState();
|
|
618
|
+
if (!state.tabsEl) return;
|
|
619
|
+
|
|
620
|
+
const messagesTab = state.tabsEl.querySelector('[data-tab="messages"]');
|
|
621
|
+
const rawTab = state.tabsEl.querySelector('[data-tab="raw"]');
|
|
622
|
+
const toolsTab = state.tabsEl.querySelector('[data-tab="tools"]');
|
|
623
|
+
const systemTab = state.tabsEl.querySelector('[data-tab="system"]');
|
|
624
|
+
|
|
625
|
+
if (messagesTab) {
|
|
626
|
+
const count = files.messages?.lines || 0;
|
|
627
|
+
messagesTab.innerHTML = `Messages <span class="tab-count">${count}</span>`;
|
|
628
|
+
}
|
|
629
|
+
if (rawTab) {
|
|
630
|
+
const count = files.raw?.lines || 0;
|
|
631
|
+
rawTab.innerHTML = `Raw <span class="tab-count">${count}</span>`;
|
|
632
|
+
}
|
|
633
|
+
if (toolsTab) {
|
|
634
|
+
const count = files.tools?.count || 0;
|
|
635
|
+
toolsTab.innerHTML = `Tools <span class="tab-count">${count}</span>`;
|
|
636
|
+
}
|
|
637
|
+
if (systemTab) {
|
|
638
|
+
// System logs are client-side, get count from session
|
|
639
|
+
const session = window.app?.sessionManager?.sessions?.find(s => s.storeId === state.sessionId);
|
|
640
|
+
const count = session?.systemLogs?.length || 0;
|
|
641
|
+
systemTab.innerHTML = `System <span class="tab-count">${count}</span>`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function switchTab(tab) {
|
|
646
|
+
const state = getState();
|
|
647
|
+
state.currentTab = tab;
|
|
648
|
+
|
|
649
|
+
// Update tab UI
|
|
650
|
+
state.tabsEl?.querySelectorAll('[data-tab]').forEach(t => {
|
|
651
|
+
t.classList.toggle('active', t.dataset.tab === tab);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
loadCurrentTab();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function showLoading() {
|
|
658
|
+
const state = getState();
|
|
659
|
+
if (state.loadingEl) state.loadingEl.hidden = false;
|
|
660
|
+
if (state.contentEl) state.contentEl.innerHTML = '';
|
|
661
|
+
if (state.emptyEl) state.emptyEl.hidden = true;
|
|
662
|
+
if (state.errorEl) state.errorEl.hidden = true;
|
|
663
|
+
if (state.loadMoreBtn) state.loadMoreBtn.hidden = true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function showError(message) {
|
|
667
|
+
const state = getState();
|
|
668
|
+
if (state.loadingEl) state.loadingEl.hidden = true;
|
|
669
|
+
if (state.emptyEl) state.emptyEl.hidden = true;
|
|
670
|
+
if (state.errorEl) {
|
|
671
|
+
state.errorEl.hidden = false;
|
|
672
|
+
state.errorEl.textContent = message;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function showEmpty(message) {
|
|
677
|
+
const state = getState();
|
|
678
|
+
if (state.loadingEl) state.loadingEl.hidden = true;
|
|
679
|
+
if (state.errorEl) state.errorEl.hidden = true;
|
|
680
|
+
if (state.contentEl) state.contentEl.innerHTML = '';
|
|
681
|
+
if (state.emptyEl) {
|
|
682
|
+
state.emptyEl.hidden = false;
|
|
683
|
+
state.emptyEl.textContent = message || 'No data';
|
|
684
|
+
}
|
|
685
|
+
if (state.loadMoreBtn) state.loadMoreBtn.hidden = true;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function updateLoadMore(hasMore) {
|
|
689
|
+
const state = getState();
|
|
690
|
+
if (state.loadMoreBtn) state.loadMoreBtn.hidden = !hasMore;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function toggleSortOrder() {
|
|
694
|
+
const state = getState();
|
|
695
|
+
state.sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc';
|
|
696
|
+
updateSortButton();
|
|
697
|
+
loadCurrentTab();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function updateSortButton() {
|
|
701
|
+
const state = getState();
|
|
702
|
+
const widget = WidgetManager.get('log-explorer');
|
|
703
|
+
if (!widget) return;
|
|
704
|
+
|
|
705
|
+
const sortAction = widget.headerActions?.find(a => a.id === 'sort');
|
|
706
|
+
if (sortAction) {
|
|
707
|
+
const icon = state.sortOrder === 'desc' ? LOG_ICONS.sortDesc : LOG_ICONS.sortAsc;
|
|
708
|
+
const title = state.sortOrder === 'desc' ? 'Newest first (click to reverse)' : 'Oldest first (click to reverse)';
|
|
709
|
+
// Update button in widget header
|
|
710
|
+
const btn = widget.container?.querySelector('[data-action="sort"]');
|
|
711
|
+
if (btn) {
|
|
712
|
+
btn.innerHTML = icon;
|
|
713
|
+
btn.setAttribute('data-tooltip', title);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function openExternal() {
|
|
719
|
+
const state = getState();
|
|
720
|
+
if (state.sessionId) {
|
|
721
|
+
const path = `sessions/${state.sessionId}/raw.jsonl`;
|
|
722
|
+
window.open(`/view?path=${encodeURIComponent(path)}`, '_blank');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
727
|
+
// Rendering: Messages Tab
|
|
728
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
729
|
+
|
|
730
|
+
function renderMessages(messages, total) {
|
|
731
|
+
const state = getState();
|
|
732
|
+
if (state.loadingEl) state.loadingEl.hidden = true;
|
|
733
|
+
if (state.errorEl) state.errorEl.hidden = true;
|
|
734
|
+
|
|
735
|
+
if (messages.length === 0) {
|
|
736
|
+
if (state.emptyEl) state.emptyEl.hidden = false;
|
|
737
|
+
if (state.contentEl) state.contentEl.innerHTML = '';
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (state.emptyEl) state.emptyEl.hidden = true;
|
|
742
|
+
|
|
743
|
+
const orderLabel = state.sortOrder === 'desc' ? 'newest first' : 'oldest first';
|
|
744
|
+
|
|
745
|
+
let html = `<div class="logs-summary">Showing ${messages.length} of ${total} messages (${orderLabel})</div>`;
|
|
746
|
+
html += '<div class="logs-messages-list">';
|
|
747
|
+
|
|
748
|
+
for (const msg of messages) {
|
|
749
|
+
html += renderMessage(msg);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
html += '</div>';
|
|
753
|
+
if (state.contentEl) state.contentEl.innerHTML = html;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function appendMessages(messages) {
|
|
757
|
+
const state = getState();
|
|
758
|
+
const list = state.contentEl?.querySelector('.logs-messages-list');
|
|
759
|
+
if (!list) return;
|
|
760
|
+
|
|
761
|
+
for (const msg of messages) {
|
|
762
|
+
list.insertAdjacentHTML('beforeend', renderMessage(msg));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Label for assistant rows: the session's own ENGINE ("Codex"), title-cased
|
|
767
|
+
// to match the other role labels. A session the manager doesn't know (a
|
|
768
|
+
// historical ID opened directly) has no engine to read, so it keeps the
|
|
769
|
+
// static strings.yaml label rather than guessing the box default.
|
|
770
|
+
function assistantLabel() {
|
|
771
|
+
const id = getState().sessionId || WidgetManager.currentSessionId;
|
|
772
|
+
const session = (window.app?.sessionManager?.sessions || [])
|
|
773
|
+
.find(s => s.id === id || s.storeId === id);
|
|
774
|
+
if (!session) return ROLE_CONFIG.assistant.label;
|
|
775
|
+
const label = engineAuthorLabel(session);
|
|
776
|
+
return escapeHtml(label.charAt(0).toUpperCase() + label.slice(1));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderMessage(msg) {
|
|
780
|
+
const role = msg.role || 'unknown';
|
|
781
|
+
const base = ROLE_CONFIG[role] || { icon: LOG_ICONS.logs, color: '#888', label: role };
|
|
782
|
+
// Copy, never mutate — ROLE_CONFIG is module-level and shared by every
|
|
783
|
+
// session's log view.
|
|
784
|
+
const config = role === 'assistant' ? { ...base, label: assistantLabel() } : base;
|
|
785
|
+
const timestamp = formatTimePrecise(msg.timestamp);
|
|
786
|
+
|
|
787
|
+
let content = '';
|
|
788
|
+
let extra = '';
|
|
789
|
+
|
|
790
|
+
switch (role) {
|
|
791
|
+
case 'user':
|
|
792
|
+
const userContent = escapeHtml(msg.content || '').slice(0, 200);
|
|
793
|
+
content = isThinkingKeywordsHighlightingEnabled() ? highlightThinkingKeywords(userContent) : userContent;
|
|
794
|
+
if (msg.has_images) {
|
|
795
|
+
extra = `<span class="log-badge">${msg.image_count} image${msg.image_count > 1 ? 's' : ''}</span>`;
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
|
|
799
|
+
case 'assistant':
|
|
800
|
+
content = escapeHtml(msg.content || '').slice(0, 200);
|
|
801
|
+
break;
|
|
802
|
+
|
|
803
|
+
case 'tool':
|
|
804
|
+
content = `<span class="log-tool-name">${escapeHtml(msg.tool_name || 'Tool')}</span>`;
|
|
805
|
+
if (msg.tool_output_file) {
|
|
806
|
+
extra = `<a href="#" data-tool-file="${escapeHtml(msg.tool_output_file)}" class="log-tool-link">View output</a>`;
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
|
|
810
|
+
case 'thinking':
|
|
811
|
+
const toolCount = (msg.tools || []).length;
|
|
812
|
+
content = `Thinking (${toolCount} tool${toolCount !== 1 ? 's' : ''})`;
|
|
813
|
+
break;
|
|
814
|
+
|
|
815
|
+
case 'result':
|
|
816
|
+
const cost = msg.cost_usd ? `$${msg.cost_usd.toFixed(4)}` : '';
|
|
817
|
+
const duration = msg.duration_ms ? `${(msg.duration_ms / 1000).toFixed(1)}s` : '';
|
|
818
|
+
const turns = msg.num_turns || 1;
|
|
819
|
+
content = `${turns} turn${turns !== 1 ? 's' : ''} | ${duration} | ${cost}`;
|
|
820
|
+
if (msg.is_error) {
|
|
821
|
+
extra = '<span class="log-badge error">Error</span>';
|
|
822
|
+
}
|
|
823
|
+
break;
|
|
824
|
+
|
|
825
|
+
default:
|
|
826
|
+
content = JSON.stringify(msg).slice(0, 100);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const isExpandable = role === 'tool' || role === 'thinking';
|
|
830
|
+
|
|
831
|
+
return `
|
|
832
|
+
<div class="log-item ${isExpandable ? 'log-item-expandable' : ''}" data-role="${role}">
|
|
833
|
+
<div class="log-item-header">
|
|
834
|
+
<span class="log-item-icon" style="color: ${config.color}">${config.icon}</span>
|
|
835
|
+
<span class="log-item-role">${config.label}</span>
|
|
836
|
+
<span class="log-item-time">${timestamp}</span>
|
|
837
|
+
${extra}
|
|
838
|
+
${isExpandable ? `<span class="log-item-chevron">${LOG_ICONS.chevron}</span>` : ''}
|
|
839
|
+
</div>
|
|
840
|
+
<div class="log-item-content">${content}</div>
|
|
841
|
+
${isExpandable ? renderExpandedContent(msg) : ''}
|
|
842
|
+
</div>
|
|
843
|
+
`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function renderExpandedContent(msg) {
|
|
847
|
+
if (msg.role === 'tool') {
|
|
848
|
+
const input = msg.tool_input ? JSON.stringify(msg.tool_input, null, 2) : '';
|
|
849
|
+
const output = msg.tool_output || '';
|
|
850
|
+
const error = msg.tool_error || '';
|
|
851
|
+
|
|
852
|
+
return `
|
|
853
|
+
<div class="log-item-expanded">
|
|
854
|
+
${input ? `<div class="log-expanded-section"><div class="log-expanded-label">Input</div><pre>${escapeHtml(input)}</pre></div>` : ''}
|
|
855
|
+
${output ? `<div class="log-expanded-section"><div class="log-expanded-label">Output</div><pre>${escapeHtml(output.slice(0, 1000))}${output.length > 1000 ? '...' : ''}</pre></div>` : ''}
|
|
856
|
+
${error ? `<div class="log-expanded-section error"><div class="log-expanded-label">Error</div><pre>${escapeHtml(error)}</pre></div>` : ''}
|
|
857
|
+
</div>
|
|
858
|
+
`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (msg.role === 'thinking') {
|
|
862
|
+
const tools = msg.tools || [];
|
|
863
|
+
let toolsHtml = tools.map(t => `
|
|
864
|
+
<div class="log-thinking-tool">
|
|
865
|
+
<span class="log-tool-name">${escapeHtml(t.toolName || 'Tool')}</span>
|
|
866
|
+
${t.toolOutput ? '<span class="log-badge">completed</span>' : '<span class="log-badge pending">pending</span>'}
|
|
867
|
+
</div>
|
|
868
|
+
`).join('');
|
|
869
|
+
|
|
870
|
+
return `
|
|
871
|
+
<div class="log-item-expanded">
|
|
872
|
+
<div class="log-expanded-section"><div class="log-expanded-label">Thinking</div><pre>${escapeHtml((msg.content || '').slice(0, 500))}${(msg.content || '').length > 500 ? '...' : ''}</pre></div>
|
|
873
|
+
${toolsHtml ? `<div class="log-expanded-section"><div class="log-expanded-label">Tools</div>${toolsHtml}</div>` : ''}
|
|
874
|
+
</div>
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return '';
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
882
|
+
// Rendering: Raw Tab
|
|
883
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
884
|
+
|
|
885
|
+
function renderRaw(entries, total) {
|
|
886
|
+
const state = getState();
|
|
887
|
+
if (state.loadingEl) state.loadingEl.hidden = true;
|
|
888
|
+
if (state.errorEl) state.errorEl.hidden = true;
|
|
889
|
+
|
|
890
|
+
if (entries.length === 0) {
|
|
891
|
+
if (state.emptyEl) state.emptyEl.hidden = false;
|
|
892
|
+
if (state.contentEl) state.contentEl.innerHTML = '';
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (state.emptyEl) state.emptyEl.hidden = true;
|
|
897
|
+
|
|
898
|
+
const orderLabel = state.sortOrder === 'desc' ? 'newest first' : 'oldest first';
|
|
899
|
+
|
|
900
|
+
let html = `
|
|
901
|
+
<div class="logs-summary">
|
|
902
|
+
Showing ${entries.length} of ${total} entries (${orderLabel})
|
|
903
|
+
<div class="logs-filters">
|
|
904
|
+
<button class="log-filter ${state.errorsOnly ? 'active' : ''}" data-filter="errors">Errors only</button>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
`;
|
|
908
|
+
html += '<div class="logs-raw-list">';
|
|
909
|
+
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
html += renderRawEntry(entry);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
html += '</div>';
|
|
915
|
+
if (state.contentEl) state.contentEl.innerHTML = html;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function appendRawEntries(entries) {
|
|
919
|
+
const state = getState();
|
|
920
|
+
const list = state.contentEl?.querySelector('.logs-raw-list');
|
|
921
|
+
if (!list) return;
|
|
922
|
+
|
|
923
|
+
for (const entry of entries) {
|
|
924
|
+
list.insertAdjacentHTML('beforeend', renderRawEntry(entry));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function renderRawEntry(entry) {
|
|
929
|
+
const dir = entry.dir || 'unknown';
|
|
930
|
+
const config = DIR_CONFIG[dir] || { icon: LOG_ICONS.logs, color: '#888', label: dir };
|
|
931
|
+
const timestamp = formatTimePrecise(entry.ts);
|
|
932
|
+
|
|
933
|
+
let content = '';
|
|
934
|
+
if (dir === 'event') {
|
|
935
|
+
content = escapeHtml(entry.event || '');
|
|
936
|
+
} else if (dir === 'error') {
|
|
937
|
+
content = `<span class="log-error-text">${escapeHtml(entry.error || '')}</span>`;
|
|
938
|
+
if (entry.context) {
|
|
939
|
+
content += `<pre class="log-error-context">${escapeHtml(entry.context.slice(0, 500))}</pre>`;
|
|
940
|
+
}
|
|
941
|
+
} else if (dir === 'in') {
|
|
942
|
+
content = `Input: ${entry.size || 0} bytes`;
|
|
943
|
+
} else if (dir === 'out') {
|
|
944
|
+
const type = entry.type || '';
|
|
945
|
+
const subtype = entry.subtype ? ` (${entry.subtype})` : '';
|
|
946
|
+
content = `<span class="log-raw-type">${type}${subtype}</span>`;
|
|
947
|
+
if (entry.truncated) {
|
|
948
|
+
content += ' <span class="log-badge">truncated</span>';
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const isExpandable = dir === 'out' && entry.data;
|
|
953
|
+
let expandedContent = '';
|
|
954
|
+
if (isExpandable) {
|
|
955
|
+
expandedContent = renderJsonData(entry.data);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Copy payload: the raw `out` JSON string verbatim when present (so what you
|
|
959
|
+
// paste matches the on-disk raw.jsonl entry), otherwise the whole entry
|
|
960
|
+
// pretty-printed. Base64 to survive quotes/newlines in a data attribute
|
|
961
|
+
// (unicode-safe, same idiom as tool-renderer-blocks.js).
|
|
962
|
+
const copyText = (dir === 'out' && entry.data)
|
|
963
|
+
? entry.data
|
|
964
|
+
: JSON.stringify(entry, null, 2);
|
|
965
|
+
const copyEncoded = btoa(unescape(encodeURIComponent(copyText)));
|
|
966
|
+
|
|
967
|
+
return `
|
|
968
|
+
<div class="log-raw-item ${isExpandable ? 'log-item-expandable' : ''}" data-dir="${dir}">
|
|
969
|
+
<div class="log-item-header">
|
|
970
|
+
<span class="log-item-icon" style="color: ${config.color}">${config.icon}</span>
|
|
971
|
+
<span class="log-item-role">${config.label}</span>
|
|
972
|
+
<span class="log-item-time">${timestamp}</span>
|
|
973
|
+
<button class="log-raw-copy" data-copy="${copyEncoded}" data-tooltip="${S.widgets.log_explorer.copy_entry}" aria-label="${S.widgets.log_explorer.copy_entry}">${LOG_ICONS.copy}</button>
|
|
974
|
+
${isExpandable ? `<span class="log-item-chevron">${LOG_ICONS.chevron}</span>` : ''}
|
|
975
|
+
</div>
|
|
976
|
+
<div class="log-item-content">${content}</div>
|
|
977
|
+
${isExpandable ? `<div class="log-item-expanded">${expandedContent}</div>` : ''}
|
|
978
|
+
</div>
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
983
|
+
// Rendering: Tools Tab
|
|
984
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
985
|
+
|
|
986
|
+
function renderTools(files) {
|
|
987
|
+
const state = getState();
|
|
988
|
+
if (state.loadingEl) state.loadingEl.hidden = true;
|
|
989
|
+
if (state.errorEl) state.errorEl.hidden = true;
|
|
990
|
+
|
|
991
|
+
if (files.length === 0) {
|
|
992
|
+
if (state.emptyEl) {
|
|
993
|
+
state.emptyEl.hidden = false;
|
|
994
|
+
state.emptyEl.textContent = 'No tool output files';
|
|
995
|
+
}
|
|
996
|
+
if (state.contentEl) state.contentEl.innerHTML = '';
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (state.emptyEl) state.emptyEl.hidden = true;
|
|
1001
|
+
|
|
1002
|
+
let html = '<div class="logs-tools-list">';
|
|
1003
|
+
|
|
1004
|
+
for (const file of files) {
|
|
1005
|
+
html += `
|
|
1006
|
+
<div class="log-tool-item" data-tool-file="${escapeHtml(file.name)}">
|
|
1007
|
+
<div class="log-tool-item-icon">${LOG_ICONS.tool}</div>
|
|
1008
|
+
<div class="log-tool-item-info">
|
|
1009
|
+
<div class="log-tool-item-name">${escapeHtml(file.tool_name || file.name)}</div>
|
|
1010
|
+
<div class="log-tool-item-meta">
|
|
1011
|
+
<span>${formatSize(file.size)}</span>
|
|
1012
|
+
<span>${formatTimePrecise(file.modified)}</span>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div class="log-tool-item-chevron">${LOG_ICONS.chevron}</div>
|
|
1016
|
+
</div>
|
|
1017
|
+
`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
html += '</div>';
|
|
1021
|
+
if (state.contentEl) state.contentEl.innerHTML = html;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1025
|
+
// JSON Tree Rendering (shared module)
|
|
1026
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1027
|
+
|
|
1028
|
+
function renderJsonData(dataStr) {
|
|
1029
|
+
if (!dataStr) return '';
|
|
1030
|
+
try {
|
|
1031
|
+
return renderJsonTree(JSON.parse(dataStr));
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
const truncated = dataStr.length > 2000;
|
|
1034
|
+
const display = truncated ? dataStr.slice(0, 2000) + '...' : dataStr;
|
|
1035
|
+
return `<pre class="json-raw">${escapeHtml(display)}</pre>`;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1040
|
+
// Public API
|
|
1041
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1042
|
+
|
|
1043
|
+
export function openLogExplorer(sessionId = null) {
|
|
1044
|
+
if (sessionId) {
|
|
1045
|
+
const state = getState(sessionId);
|
|
1046
|
+
state.sessionId = sessionId;
|
|
1047
|
+
}
|
|
1048
|
+
const widget = WidgetManager.get('log-explorer');
|
|
1049
|
+
widget?.open();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export function closeLogExplorer() {
|
|
1053
|
+
const widget = WidgetManager.get('log-explorer');
|
|
1054
|
+
widget?.close();
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export { destroyState as destroyLogExplorerState };
|