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,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Manager Module
|
|
3
|
+
* Manages scroll position, "new messages" indicator, and lazy loading trigger
|
|
4
|
+
* Implements Slack-style scroll behavior
|
|
5
|
+
*
|
|
6
|
+
* Uses ScrollStateMachine for race-free state management:
|
|
7
|
+
* - No more boolean flag races during tab switches
|
|
8
|
+
* - Coalesced scroll operations via requestAnimationFrame
|
|
9
|
+
* - Explicit state transitions prevent invalid operations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ScrollStateMachine, ScrollState } from './scroll-state-machine.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ScrollManager - Handle scroll position and new messages indicator
|
|
16
|
+
*/
|
|
17
|
+
export class ScrollManager {
|
|
18
|
+
/**
|
|
19
|
+
* @param {HTMLElement} container - The scrollable messages container
|
|
20
|
+
* @param {Object} options - Configuration options
|
|
21
|
+
* @param {number} options.scrollThreshold - Pixels from bottom to consider "at bottom" (default: 100)
|
|
22
|
+
* @param {number} options.topThreshold - Pixels from top to trigger load more (default: 100)
|
|
23
|
+
* @param {Object} callbacks - Callback functions
|
|
24
|
+
* @param {Function} callbacks.onScrollTop - Called when user scrolls near top (for lazy loading)
|
|
25
|
+
* @param {Function} callbacks.getSession - Returns current session (for scroll position persistence)
|
|
26
|
+
*/
|
|
27
|
+
constructor(container, options = {}, callbacks = {}) {
|
|
28
|
+
this.container = container;
|
|
29
|
+
|
|
30
|
+
// Configuration
|
|
31
|
+
this.scrollThreshold = options.scrollThreshold || 100;
|
|
32
|
+
this.topThreshold = options.topThreshold || 100;
|
|
33
|
+
|
|
34
|
+
// Callbacks
|
|
35
|
+
this.onScrollTop = callbacks.onScrollTop || (() => {});
|
|
36
|
+
this.getSession = callbacks.getSession || (() => null);
|
|
37
|
+
|
|
38
|
+
// State machine for scroll operations (replaces boolean flags)
|
|
39
|
+
this._stateMachine = new ScrollStateMachine();
|
|
40
|
+
|
|
41
|
+
// Legacy compatibility: expose isUserScrolledUp as getter
|
|
42
|
+
// Actual state tracked via state machine
|
|
43
|
+
this._isUserScrolledUp = false;
|
|
44
|
+
|
|
45
|
+
this.newMessageCount = 0;
|
|
46
|
+
this._lastScrollTopCall = 0; // Throttle for onScrollTop
|
|
47
|
+
this._rafId = null; // RAF throttling for scroll
|
|
48
|
+
|
|
49
|
+
// PERF: Cache container rect to avoid repeated layout queries
|
|
50
|
+
this._containerRect = null;
|
|
51
|
+
this._containerRectTime = 0;
|
|
52
|
+
|
|
53
|
+
// Bind scroll event with RAF throttling for performance
|
|
54
|
+
this._scrollHandler = () => {
|
|
55
|
+
// Cancel any pending RAF - only process latest scroll position
|
|
56
|
+
if (this._rafId) return;
|
|
57
|
+
this._rafId = requestAnimationFrame(() => {
|
|
58
|
+
this._rafId = null;
|
|
59
|
+
this._handleScroll();
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
this.container.addEventListener('scroll', this._scrollHandler);
|
|
63
|
+
|
|
64
|
+
// Hold auto-scroll while a real mouse/trackpad pointer rests on a
|
|
65
|
+
// link in the transcript — streaming updates otherwise yank the link
|
|
66
|
+
// out from under the cursor mid-click. Tracked via pointer events
|
|
67
|
+
// gated on pointerType 'mouse' (NOT a CSS :hover probe: iOS taps
|
|
68
|
+
// leave sticky :hover on links, which would wedge auto-scroll off
|
|
69
|
+
// after every tapped link on touch devices).
|
|
70
|
+
this._hoveredLink = null;
|
|
71
|
+
document.addEventListener('pointerover', (e) => {
|
|
72
|
+
if (e.pointerType !== 'mouse') return;
|
|
73
|
+
this._hoveredLink = e.target.closest?.('a') || null;
|
|
74
|
+
});
|
|
75
|
+
document.addEventListener('pointerout', (e) => {
|
|
76
|
+
if (e.pointerType !== 'mouse') return;
|
|
77
|
+
// Pointer left the window — no pointerover follows to clear it
|
|
78
|
+
if (!e.relatedTarget) this._hoveredLink = null;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Is the mouse pointer currently on a link inside the transcript?
|
|
84
|
+
* isConnected guards against streaming re-renders replacing the node
|
|
85
|
+
* without a pointer boundary event firing.
|
|
86
|
+
*/
|
|
87
|
+
_isPointerOnLink() {
|
|
88
|
+
const link = this._hoveredLink;
|
|
89
|
+
return !!(link && link.isConnected && this.container.contains(link));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Legacy getter for backwards compatibility
|
|
93
|
+
get isUserScrolledUp() {
|
|
94
|
+
return this._isUserScrolledUp;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
set isUserScrolledUp(value) {
|
|
98
|
+
this._isUserScrolledUp = value;
|
|
99
|
+
// Sync with state machine
|
|
100
|
+
if (value && this._stateMachine.getState() === ScrollState.IDLE) {
|
|
101
|
+
this._stateMachine.transition(ScrollState.USER_SCROLLING);
|
|
102
|
+
} else if (!value && this._stateMachine.getState() === ScrollState.USER_SCROLLING) {
|
|
103
|
+
this._stateMachine.transition(ScrollState.IDLE);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle scroll events
|
|
109
|
+
*/
|
|
110
|
+
_handleScroll() {
|
|
111
|
+
// Skip during tab/session switch (use state machine)
|
|
112
|
+
if (this._stateMachine.isSwitching()) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const wasScrolledUp = this._isUserScrolledUp;
|
|
117
|
+
this._isUserScrolledUp = !this.isNearBottom();
|
|
118
|
+
|
|
119
|
+
// User scrolled back to bottom - clear indicator and update state machine
|
|
120
|
+
if (wasScrolledUp && !this._isUserScrolledUp) {
|
|
121
|
+
this.clearIndicator();
|
|
122
|
+
this._stateMachine.userScrolledToBottom();
|
|
123
|
+
} else if (!wasScrolledUp && this._isUserScrolledUp) {
|
|
124
|
+
// User just scrolled up - update state machine
|
|
125
|
+
this._stateMachine.userScrollStart();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update button visibility
|
|
129
|
+
this._updateButton();
|
|
130
|
+
|
|
131
|
+
// Update question indicator on scroll (check if question is now visible)
|
|
132
|
+
if (this._pendingQuestion) {
|
|
133
|
+
this.updateQuestionIndicator(this._pendingQuestion);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Lazy loading: check if scrolled near top (throttled to prevent spam)
|
|
137
|
+
if (this._isNearTop()) {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
// Throttle: only call once per 500ms
|
|
140
|
+
if (now - this._lastScrollTopCall > 500) {
|
|
141
|
+
this._lastScrollTopCall = now;
|
|
142
|
+
this.onScrollTop();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if user is scrolled near the bottom
|
|
149
|
+
*/
|
|
150
|
+
isNearBottom() {
|
|
151
|
+
const distanceFromBottom = this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight;
|
|
152
|
+
return distanceFromBottom <= this.scrollThreshold;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if user has scrolled near the top
|
|
157
|
+
* Note: On iOS Safari, scrollTop can be negative due to rubber-band effect
|
|
158
|
+
*/
|
|
159
|
+
_isNearTop() {
|
|
160
|
+
// Ignore negative scrollTop (iOS rubber-band effect)
|
|
161
|
+
const scrollTop = Math.max(0, this.container.scrollTop);
|
|
162
|
+
return scrollTop < this.topThreshold;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Update the floating "new messages" / "scroll to bottom" button
|
|
167
|
+
*/
|
|
168
|
+
_updateButton() {
|
|
169
|
+
// PERF: Use cached button reference instead of querying every scroll
|
|
170
|
+
let btn = this._newMessagesBtn;
|
|
171
|
+
|
|
172
|
+
// Show button whenever user is scrolled up
|
|
173
|
+
if (this.isUserScrolledUp) {
|
|
174
|
+
if (!btn) {
|
|
175
|
+
btn = document.createElement('button');
|
|
176
|
+
btn.className = 'new-messages-btn';
|
|
177
|
+
btn.addEventListener('click', () => this.scrollToBottomForce());
|
|
178
|
+
this.container.appendChild(btn);
|
|
179
|
+
this._newMessagesBtn = btn; // Cache reference
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this.newMessageCount > 0) {
|
|
183
|
+
// New messages - highlight style
|
|
184
|
+
const plural = this.newMessageCount === 1 ? '' : 's';
|
|
185
|
+
btn.innerHTML = `
|
|
186
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
187
|
+
<path d="M12 19V5M5 12l7 7 7-7"/>
|
|
188
|
+
</svg>
|
|
189
|
+
${this.newMessageCount} new message${plural}
|
|
190
|
+
`;
|
|
191
|
+
btn.classList.remove('subtle');
|
|
192
|
+
} else {
|
|
193
|
+
// No new messages - subtle style
|
|
194
|
+
btn.innerHTML = `
|
|
195
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
196
|
+
<path d="M12 19V5M5 12l7 7 7-7"/>
|
|
197
|
+
</svg>
|
|
198
|
+
Jump to latest
|
|
199
|
+
`;
|
|
200
|
+
btn.classList.add('subtle');
|
|
201
|
+
}
|
|
202
|
+
btn.classList.add('visible');
|
|
203
|
+
} else if (btn) {
|
|
204
|
+
btn.classList.remove('visible');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────
|
|
209
|
+
// Public API
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set session switching flag (to pause scroll handling)
|
|
214
|
+
* Uses state machine for proper state transitions
|
|
215
|
+
*/
|
|
216
|
+
setSwitching(switching) {
|
|
217
|
+
if (switching) {
|
|
218
|
+
this._stateMachine.beginSwitch();
|
|
219
|
+
} else {
|
|
220
|
+
this._stateMachine.endSwitch();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Retarget to a new scroll container (per-tab scroll architecture).
|
|
226
|
+
* Moves the scroll listener and floating elements to the new container.
|
|
227
|
+
* @param {HTMLElement} newContainer - The new scrollable element
|
|
228
|
+
* @param {Object} [options]
|
|
229
|
+
* @param {boolean} [options.skipInitState] - Skip initState() when restoring
|
|
230
|
+
* scroll state externally (e.g., O(1) tab switch where session.isUserScrolledUp is used)
|
|
231
|
+
*/
|
|
232
|
+
setContainer(newContainer, options = {}) {
|
|
233
|
+
if (newContainer === this.container) return;
|
|
234
|
+
|
|
235
|
+
// Move event listener
|
|
236
|
+
this.container.removeEventListener('scroll', this._scrollHandler);
|
|
237
|
+
newContainer.addEventListener('scroll', this._scrollHandler);
|
|
238
|
+
|
|
239
|
+
// Move floating elements (new-messages button, question indicator)
|
|
240
|
+
if (this._newMessagesBtn?.parentNode) {
|
|
241
|
+
this._newMessagesBtn.remove();
|
|
242
|
+
newContainer.appendChild(this._newMessagesBtn);
|
|
243
|
+
}
|
|
244
|
+
if (this._questionIndicator?.parentNode) {
|
|
245
|
+
this._questionIndicator.remove();
|
|
246
|
+
newContainer.appendChild(this._questionIndicator);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.container = newContainer;
|
|
250
|
+
this._containerRect = null; // Invalidate cached rect
|
|
251
|
+
|
|
252
|
+
if (!options.skipInitState) {
|
|
253
|
+
this.initState();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Soft scroll to bottom - only scrolls if user is already near bottom
|
|
259
|
+
* Does NOT track new messages (use trackNewMessage for that)
|
|
260
|
+
* Uses state machine to prevent races
|
|
261
|
+
*/
|
|
262
|
+
scrollToBottom() {
|
|
263
|
+
// Pointer is aiming at a link — don't move it (checked before the
|
|
264
|
+
// state machine so the streaming throttle stamp isn't consumed)
|
|
265
|
+
if (this._isPointerOnLink()) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Use state machine to check if we can auto-scroll
|
|
269
|
+
if (!this._stateMachine.canAutoScroll()) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// If user is scrolled up, don't auto-scroll
|
|
273
|
+
if (this._isUserScrolledUp) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Use state machine's coalesced scroll
|
|
278
|
+
this._stateMachine.scrollToBottom(this.container);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Soft scroll during streaming - throttled to prevent excessive scrolls
|
|
283
|
+
*/
|
|
284
|
+
scrollToBottomStreaming() {
|
|
285
|
+
if (this._isPointerOnLink()) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (!this._stateMachine.canAutoScrollStreaming(100)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (this._isUserScrolledUp) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this._stateMachine.scrollToBottom(this.container, { streaming: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Track that a new meaningful message arrived (for the indicator)
|
|
300
|
+
* Only call this for actual assistant responses, not tool updates or typing
|
|
301
|
+
*/
|
|
302
|
+
trackNewMessage() {
|
|
303
|
+
if (this.isUserScrolledUp) {
|
|
304
|
+
this.newMessageCount++;
|
|
305
|
+
this._updateButton();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Force scroll to bottom - always scrolls (user action)
|
|
311
|
+
* Used when user clicks "new messages" button or sends a message
|
|
312
|
+
* @param {Object} options
|
|
313
|
+
* @param {boolean} options.respectLinkHover - UI-initiated force scrolls
|
|
314
|
+
* (activity strip growth) pass this so they can't yank a link out from
|
|
315
|
+
* under the pointer; genuine user actions omit it and always scroll.
|
|
316
|
+
*/
|
|
317
|
+
scrollToBottomForce(options = {}) {
|
|
318
|
+
if (options.respectLinkHover && this._isPointerOnLink()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.clearIndicator();
|
|
322
|
+
this._isUserScrolledUp = false;
|
|
323
|
+
|
|
324
|
+
// Reset state machine to IDLE
|
|
325
|
+
if (this._stateMachine.getState() === ScrollState.USER_SCROLLING) {
|
|
326
|
+
this._stateMachine.transition(ScrollState.IDLE);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Clear saved scroll position on session
|
|
330
|
+
const session = this.getSession();
|
|
331
|
+
if (session) {
|
|
332
|
+
session.scrollPosition = null;
|
|
333
|
+
session.isUserScrolledUp = false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Use state machine's forced scroll
|
|
337
|
+
this._stateMachine.scrollToBottom(this.container, { force: true });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Clear the new messages counter and remove the button
|
|
342
|
+
*/
|
|
343
|
+
clearIndicator() {
|
|
344
|
+
this.newMessageCount = 0;
|
|
345
|
+
// PERF: Use cached reference instead of querying
|
|
346
|
+
const btn = this._newMessagesBtn;
|
|
347
|
+
if (btn) {
|
|
348
|
+
btn.remove();
|
|
349
|
+
this._newMessagesBtn = null; // Clear cache
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Initialize scroll state based on current position
|
|
355
|
+
* Call after rendering messages
|
|
356
|
+
*/
|
|
357
|
+
initState() {
|
|
358
|
+
this._isUserScrolledUp = !this.isNearBottom();
|
|
359
|
+
// Sync with state machine
|
|
360
|
+
if (this._isUserScrolledUp) {
|
|
361
|
+
this._stateMachine.forceTransition(ScrollState.USER_SCROLLING, 'initState');
|
|
362
|
+
} else {
|
|
363
|
+
this._stateMachine.forceTransition(ScrollState.IDLE, 'initState');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get current scroll position for persistence
|
|
369
|
+
*/
|
|
370
|
+
getScrollPosition() {
|
|
371
|
+
return this.container.scrollTop;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Restore scroll position using state machine
|
|
376
|
+
*/
|
|
377
|
+
setScrollPosition(position) {
|
|
378
|
+
this._stateMachine.savePosition(this.container);
|
|
379
|
+
this._stateMachine.scheduleScroll(this.container, position);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Scroll to absolute bottom (for restoring at-bottom state)
|
|
384
|
+
*/
|
|
385
|
+
scrollToMax() {
|
|
386
|
+
this._stateMachine.scrollToBottom(this.container, { force: true });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get state machine state (for debugging)
|
|
391
|
+
*/
|
|
392
|
+
getState() {
|
|
393
|
+
return this._stateMachine.toJSON();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─────────────────────────────────────────────────────────────────
|
|
397
|
+
// Question Indicator
|
|
398
|
+
// ─────────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Update the floating question indicator
|
|
402
|
+
* Shows when there's a pending question that's not visible in viewport
|
|
403
|
+
* @param {Object|null} questionMsg - The pending question message, or null if none
|
|
404
|
+
*/
|
|
405
|
+
updateQuestionIndicator(questionMsg) {
|
|
406
|
+
// Store for scroll handler
|
|
407
|
+
this._pendingQuestion = questionMsg;
|
|
408
|
+
|
|
409
|
+
// PERF: Use cached indicator reference instead of querying
|
|
410
|
+
let indicator = this._questionIndicator;
|
|
411
|
+
|
|
412
|
+
// No question or question is answered - hide indicator
|
|
413
|
+
if (!questionMsg || questionMsg.answered) {
|
|
414
|
+
this._pendingQuestion = null;
|
|
415
|
+
if (indicator) indicator.classList.remove('visible');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check if question element is in viewport
|
|
420
|
+
const questionEl = document.getElementById(`msg-${questionMsg.id}`);
|
|
421
|
+
if (questionEl && this._isElementInViewport(questionEl)) {
|
|
422
|
+
if (indicator) indicator.classList.remove('visible');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Question exists but not visible - show indicator
|
|
427
|
+
if (!indicator) {
|
|
428
|
+
indicator = document.createElement('div');
|
|
429
|
+
indicator.className = 'question-indicator';
|
|
430
|
+
indicator.addEventListener('click', () => this._scrollToQuestion(questionMsg));
|
|
431
|
+
this.container.appendChild(indicator);
|
|
432
|
+
this._questionIndicator = indicator; // Cache reference
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Build badge previews from question entries
|
|
436
|
+
const entries = questionMsg.entries || [{
|
|
437
|
+
questions: questionMsg.questions || []
|
|
438
|
+
}];
|
|
439
|
+
|
|
440
|
+
// Collect all question headers/badges
|
|
441
|
+
const badges = [];
|
|
442
|
+
entries.forEach(entry => {
|
|
443
|
+
(Array.isArray(entry.questions) ? entry.questions : []).forEach(q => {
|
|
444
|
+
if (q.header) badges.push(q.header);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Limit to first 4 badges
|
|
449
|
+
const displayBadges = badges.slice(0, 4);
|
|
450
|
+
const moreBadges = badges.length > 4 ? badges.length - 4 : 0;
|
|
451
|
+
|
|
452
|
+
const badgesHtml = displayBadges.map(b =>
|
|
453
|
+
`<span class="qi-badge">${this._escapeHtml(b)}</span>`
|
|
454
|
+
).join('') + (moreBadges > 0 ? `<span class="qi-more">+${moreBadges}</span>` : '');
|
|
455
|
+
|
|
456
|
+
indicator.innerHTML = `
|
|
457
|
+
<div class="qi-icon">
|
|
458
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
459
|
+
<circle cx="12" cy="12" r="10"/>
|
|
460
|
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
|
461
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
462
|
+
</svg>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="qi-content">
|
|
465
|
+
<div class="qi-title">Claude is asking</div>
|
|
466
|
+
<div class="qi-badges">${badgesHtml}</div>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="qi-arrow">
|
|
469
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
470
|
+
<path d="M12 19V5M5 12l7-7 7 7"/>
|
|
471
|
+
</svg>
|
|
472
|
+
</div>
|
|
473
|
+
`;
|
|
474
|
+
|
|
475
|
+
indicator.classList.add('visible');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get cached container bounding rect (expires after 100ms)
|
|
480
|
+
* PERF: Avoids repeated getBoundingClientRect calls during scroll
|
|
481
|
+
*/
|
|
482
|
+
_getContainerRect() {
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
if (!this._containerRect || now - this._containerRectTime > 100) {
|
|
485
|
+
this._containerRect = this.container.getBoundingClientRect();
|
|
486
|
+
this._containerRectTime = now;
|
|
487
|
+
}
|
|
488
|
+
return this._containerRect;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Check if an element is visible in the viewport
|
|
493
|
+
*/
|
|
494
|
+
_isElementInViewport(el) {
|
|
495
|
+
const rect = el.getBoundingClientRect();
|
|
496
|
+
const containerRect = this._getContainerRect();
|
|
497
|
+
|
|
498
|
+
// Check if element is within container's visible area
|
|
499
|
+
return rect.top < containerRect.bottom && rect.bottom > containerRect.top;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Scroll to the question element
|
|
504
|
+
*/
|
|
505
|
+
_scrollToQuestion(questionMsg) {
|
|
506
|
+
const questionEl = document.getElementById(`msg-${questionMsg.id}`);
|
|
507
|
+
if (questionEl) {
|
|
508
|
+
questionEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
509
|
+
// Brief highlight effect
|
|
510
|
+
questionEl.classList.add('highlight-pulse');
|
|
511
|
+
setTimeout(() => questionEl.classList.remove('highlight-pulse'), 1500);
|
|
512
|
+
}
|
|
513
|
+
// Hide indicator (use cached reference)
|
|
514
|
+
if (this._questionIndicator) {
|
|
515
|
+
this._questionIndicator.classList.remove('visible');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Simple HTML escape (PERF: use string replacement instead of DOM)
|
|
521
|
+
*/
|
|
522
|
+
_escapeHtml(str) {
|
|
523
|
+
return str
|
|
524
|
+
.replace(/&/g, '&')
|
|
525
|
+
.replace(/</g, '<')
|
|
526
|
+
.replace(/>/g, '>')
|
|
527
|
+
.replace(/"/g, '"')
|
|
528
|
+
.replace(/'/g, ''');
|
|
529
|
+
}
|
|
530
|
+
}
|