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,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Widget — interactive terminal with xterm.js, mounted via the
|
|
3
|
+
* widget system. Top-sheet on phone/tablet, floating on desktop, and
|
|
4
|
+
* promotable to a tab. Provides a quick-access PTY for the project's
|
|
5
|
+
* CWD, persistent sessions keyed by `(widget session, cwd)`, and a
|
|
6
|
+
* separate per-tab state for the multi-terminal tab strip.
|
|
7
|
+
*
|
|
8
|
+
* This file is the orchestrator: it composes the sub-modules under
|
|
9
|
+
* `widgets/terminal/` and exposes the public surface (`registerTerminalWidget`
|
|
10
|
+
* and the `TerminalWidget` namespace consumed by app.js, tab-controller,
|
|
11
|
+
* orphan-terminals, quick-actions, and the Settings widget). All heavy
|
|
12
|
+
* logic — state, gestures, link providers, init, connection, render,
|
|
13
|
+
* size — lives in dedicated modules so this file stays focused on
|
|
14
|
+
* widget configuration, lifecycle hooks, and the float→tab transfer.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { debug } from '../config.js';
|
|
18
|
+
import { ContextMenu } from '../context-menu.js';
|
|
19
|
+
import S from '../strings.js';
|
|
20
|
+
import { WidgetManager } from '../widget-system/index.js';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
destroySessionState,
|
|
24
|
+
fitTerminal,
|
|
25
|
+
getOrCreateTabState,
|
|
26
|
+
getSessionState,
|
|
27
|
+
lastFocusedIfRecent,
|
|
28
|
+
nextTabNumber,
|
|
29
|
+
removeTabState,
|
|
30
|
+
sessionStates,
|
|
31
|
+
tabStates,
|
|
32
|
+
} from './terminal/state.js';
|
|
33
|
+
import {
|
|
34
|
+
copyToClipboard,
|
|
35
|
+
getUnwrappedTerminalSelection,
|
|
36
|
+
} from './terminal/gestures.js';
|
|
37
|
+
import { FilePathLinkProvider, makeLiveCwdRefresher } from './terminal/link-providers.js';
|
|
38
|
+
import {
|
|
39
|
+
connect,
|
|
40
|
+
handleJsonMessage,
|
|
41
|
+
killTerminal,
|
|
42
|
+
updateStatus,
|
|
43
|
+
} from './terminal/connection.js';
|
|
44
|
+
import { initTerminal } from './terminal/init.js';
|
|
45
|
+
import { renderContent } from './terminal/render.js';
|
|
46
|
+
import {
|
|
47
|
+
DEFAULT_TERMINAL_HEIGHT,
|
|
48
|
+
DEFAULT_TERMINAL_WIDTH,
|
|
49
|
+
getConfiguredSize,
|
|
50
|
+
resetConfiguredSize,
|
|
51
|
+
setConfiguredSize,
|
|
52
|
+
} from './terminal/size.js';
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Header context menu (reuses shared ContextMenu from context-menu.js)
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
let headerContextMenu = null;
|
|
59
|
+
|
|
60
|
+
function showTerminalHeaderMenu(x, y, widget) {
|
|
61
|
+
if (!headerContextMenu) headerContextMenu = new ContextMenu();
|
|
62
|
+
|
|
63
|
+
const dims = widget.getDimensions();
|
|
64
|
+
const configured = getConfiguredSize();
|
|
65
|
+
const isSameSize = dims.width === configured.width && dims.height === configured.height;
|
|
66
|
+
|
|
67
|
+
headerContextMenu.show(x, y, [
|
|
68
|
+
{
|
|
69
|
+
label: `Save current size as default (${dims.width}×${dims.height})`,
|
|
70
|
+
disabled: isSameSize,
|
|
71
|
+
action: () => {
|
|
72
|
+
setConfiguredSize(dims.width, dims.height);
|
|
73
|
+
widget._defaultSize = { width: dims.width, height: dims.height };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: `Restore default (${configured.width}×${configured.height})`,
|
|
78
|
+
disabled: isSameSize,
|
|
79
|
+
action: () => {
|
|
80
|
+
widget.size = { ...configured };
|
|
81
|
+
widget.position = {
|
|
82
|
+
x: Math.round((window.innerWidth - configured.width) / 2),
|
|
83
|
+
y: Math.round((window.innerHeight - configured.height) / 2)
|
|
84
|
+
};
|
|
85
|
+
widget.constrainPosition();
|
|
86
|
+
widget.updatePosition();
|
|
87
|
+
widget.updateSize();
|
|
88
|
+
widget.config.onResize?.();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
// Widget Registration
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export function registerTerminalWidget() {
|
|
99
|
+
const configured = getConfiguredSize();
|
|
100
|
+
|
|
101
|
+
WidgetManager.register('terminal', {
|
|
102
|
+
type: 'top-sheet',
|
|
103
|
+
title: S.widgets.titles.terminal,
|
|
104
|
+
icon: 'terminal',
|
|
105
|
+
shortcut: 'Ctrl+`',
|
|
106
|
+
|
|
107
|
+
// Device-specific types
|
|
108
|
+
deviceTypes: {
|
|
109
|
+
default: 'top-sheet',
|
|
110
|
+
phone: 'top-sheet',
|
|
111
|
+
tablet: 'top-sheet',
|
|
112
|
+
desktop: 'floating'
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Heights for top-sheet
|
|
116
|
+
heights: {
|
|
117
|
+
half: '30vh',
|
|
118
|
+
full: '50vh'
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Floating window size — double-click header to restore to configured size
|
|
122
|
+
size: { width: configured.width, height: configured.height },
|
|
123
|
+
minSize: { width: 400, height: 200 },
|
|
124
|
+
|
|
125
|
+
// Allow transform to these types (including tab via widget system)
|
|
126
|
+
allowedTypes: ['top-sheet', 'floating', 'tab'],
|
|
127
|
+
|
|
128
|
+
// Custom header actions
|
|
129
|
+
headerActions: [
|
|
130
|
+
{
|
|
131
|
+
icon: 'plus',
|
|
132
|
+
title: S.widgets.header_actions.new_terminal_tab,
|
|
133
|
+
onClick: () => window.app?.tabCtrl?.openTerminalWidgetTab()
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
icon: 'kill',
|
|
137
|
+
title: S.widgets.header_actions.kill_terminal,
|
|
138
|
+
onClick: () => killTerminal(getSessionState())
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
|
|
142
|
+
// Render function - uses appropriate state based on context
|
|
143
|
+
render: (container, ctx) => {
|
|
144
|
+
let targetState;
|
|
145
|
+
|
|
146
|
+
if (ctx.isTab && ctx.tabId) {
|
|
147
|
+
// Widget tab render - get or create state for this specific tab
|
|
148
|
+
// Pass sessionId from ctx for reconnecting to existing PTY on page reload
|
|
149
|
+
targetState = getOrCreateTabState(ctx.tabId, ctx.cwd, ctx.terminalSessionId);
|
|
150
|
+
|
|
151
|
+
// One-shot command to type after the PTY connects (e.g. `/login` flow).
|
|
152
|
+
// Only set on first render — cleared after send so reconnects don't replay.
|
|
153
|
+
if (ctx.initialCommand && !targetState.terminal) {
|
|
154
|
+
targetState.pendingInitialCommand = ctx.initialCommand;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if this is a promotion from floating (transfer flag set)
|
|
158
|
+
const sourceState = getSessionState();
|
|
159
|
+
if (ctx.transferFromFloating && sourceState.terminal && !targetState.terminal) {
|
|
160
|
+
transferFloatingToTab(sourceState, targetState);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Session (floating) terminal - get per-session state
|
|
164
|
+
targetState = getSessionState(ctx.sessionId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Update CWD from context
|
|
168
|
+
const newCwd = ctx.cwd || window.app?.activeSession?.cwd;
|
|
169
|
+
if (newCwd !== targetState.cwd) {
|
|
170
|
+
targetState.cwd = newCwd;
|
|
171
|
+
// If terminal exists and CWD changed, reconnect
|
|
172
|
+
if (targetState.initialized && targetState.connected) {
|
|
173
|
+
connect(targetState);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
renderContent(container, targetState);
|
|
178
|
+
|
|
179
|
+
// Initialize terminal if it doesn't exist yet (new session or new tab)
|
|
180
|
+
// No CWD guard — backend falls back to agents.default_cwd when no CWD is sent
|
|
181
|
+
if (!targetState.terminal) {
|
|
182
|
+
initTerminal(targetState);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Keyboard handlers (Ctrl+C, Cmd+C, Cmd+V) are attached to the Terminal instance
|
|
186
|
+
// via attachCustomKeyEventHandler in initTerminal(), so they survive container re-renders
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Open handler - initialize session's terminal
|
|
190
|
+
onOpen: () => {
|
|
191
|
+
const sState = getSessionState();
|
|
192
|
+
|
|
193
|
+
// Get CWD from active session (may be empty on welcome screen — that's OK,
|
|
194
|
+
// backend falls back to agents.default_cwd)
|
|
195
|
+
if (!sState.cwd) {
|
|
196
|
+
sState.cwd = window.app?.activeSession?.cwd || null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Blur any active terminal tab to release keyboard capture
|
|
200
|
+
if (window.app?.terminalTabManager?.activeTabId) {
|
|
201
|
+
const activeTab = window.app.terminalTabManager.tabs.find(
|
|
202
|
+
t => t.id === window.app.terminalTabManager.activeTabId
|
|
203
|
+
);
|
|
204
|
+
if (activeTab?.terminal) {
|
|
205
|
+
activeTab.terminal.blur();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Also blur all widget tab terminals
|
|
209
|
+
for (const [, tabState] of tabStates) {
|
|
210
|
+
if (tabState.terminal) {
|
|
211
|
+
tabState.terminal.blur();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Initialize this session's terminal
|
|
216
|
+
initTerminal(sState);
|
|
217
|
+
|
|
218
|
+
// Ensure widget container has pointer-events (belt and suspenders)
|
|
219
|
+
const widget = WidgetManager.get('terminal');
|
|
220
|
+
if (widget?.container) {
|
|
221
|
+
widget.container.style.pointerEvents = 'auto';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Attach header context menu (once)
|
|
225
|
+
if (widget?.headerEl && !widget.headerEl._termCtxMenu) {
|
|
226
|
+
widget.headerEl._termCtxMenu = true;
|
|
227
|
+
widget.headerEl.addEventListener('contextmenu', (e) => {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
showTerminalHeaderMenu(e.clientX, e.clientY, widget);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Hide connection bar when terminal is open (saves space, especially on iPad)
|
|
234
|
+
const connBar = document.getElementById('connection-bar');
|
|
235
|
+
if (connBar) {
|
|
236
|
+
connBar.classList.add('hidden-by-terminal');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Focus terminal after animation settles so iOS keyboard
|
|
240
|
+
// viewport shift doesn't race with top-sheet slide-in
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (sState.terminal) {
|
|
243
|
+
sState.terminal.focus();
|
|
244
|
+
}
|
|
245
|
+
}, 350);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Close handler - focus chat input
|
|
249
|
+
onClose: () => {
|
|
250
|
+
// Reset keyboard bar modifiers (unlock Ctrl/Alt, dismiss popups)
|
|
251
|
+
const sState = getSessionState();
|
|
252
|
+
if (sState.keyboardBar) {
|
|
253
|
+
sState.keyboardBar.resetModifiers();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Clear inline pointer-events (set in onOpen) to let CSS handle visibility
|
|
257
|
+
const widget = WidgetManager.get('terminal');
|
|
258
|
+
if (widget?.container) {
|
|
259
|
+
widget.container.style.pointerEvents = '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Restore connection bar ONLY if no terminal tab is open
|
|
263
|
+
// (when transferring floating→tab, the tab is pushed before close() fires)
|
|
264
|
+
const hasTerminalTab = window.app?.tabCtrl?.widgetTabs?.some(t => t.isTerminal);
|
|
265
|
+
if (!hasTerminalTab) {
|
|
266
|
+
const connBar = document.getElementById('connection-bar');
|
|
267
|
+
if (connBar) {
|
|
268
|
+
connBar.classList.remove('hidden-by-terminal');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const chatInput = document.getElementById('message-input');
|
|
273
|
+
if (chatInput) {
|
|
274
|
+
setTimeout(() => chatInput.focus(), 100);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// Resize handler - refit session's terminal
|
|
279
|
+
onResize: () => {
|
|
280
|
+
const sState = getSessionState();
|
|
281
|
+
setTimeout(() => fitTerminal(sState), 50);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
// Transform handler - refit after transform
|
|
285
|
+
onTransform: (fromType, toType) => {
|
|
286
|
+
const sState = getSessionState();
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
fitTerminal(sState);
|
|
289
|
+
if (sState.terminal) {
|
|
290
|
+
sState.terminal.focus();
|
|
291
|
+
}
|
|
292
|
+
}, 100);
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Session change handler - switch terminal to new session's state
|
|
296
|
+
onSessionChange: (sessionId) => {
|
|
297
|
+
const sState = getSessionState(sessionId);
|
|
298
|
+
const newCwd = WidgetManager.currentCwd || window.app?.activeSession?.cwd;
|
|
299
|
+
if (newCwd && newCwd !== sState.cwd) {
|
|
300
|
+
// Clear path cache when CWD changes
|
|
301
|
+
if (sState.filePathProvider) {
|
|
302
|
+
sState.filePathProvider.clearCache();
|
|
303
|
+
}
|
|
304
|
+
sState.cwd = newCwd;
|
|
305
|
+
// Reconnect if widget is visible and terminal is initialized
|
|
306
|
+
const widget = WidgetManager.get('terminal');
|
|
307
|
+
if (widget?.isVisible && sState.initialized) {
|
|
308
|
+
connect(sState);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// CWD change handler - update terminal when project directory changes
|
|
314
|
+
onCwdChange: (cwd) => {
|
|
315
|
+
const sState = getSessionState();
|
|
316
|
+
if (cwd && cwd !== sState.cwd) {
|
|
317
|
+
// Clear path cache when CWD changes
|
|
318
|
+
if (sState.filePathProvider) {
|
|
319
|
+
sState.filePathProvider.clearCache();
|
|
320
|
+
}
|
|
321
|
+
sState.cwd = cwd;
|
|
322
|
+
// Reconnect if widget is visible and terminal is initialized
|
|
323
|
+
const widget = WidgetManager.get('terminal');
|
|
324
|
+
if (widget?.isVisible && sState.initialized) {
|
|
325
|
+
connect(sState);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// Cleanup when session is closed
|
|
331
|
+
onDestroy: (sessionId) => {
|
|
332
|
+
destroySessionState(sessionId);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Transfer a live xterm/WebSocket from the session's floating terminal
|
|
339
|
+
* to a tab terminal. This is what powers "Open in Tab" — the user
|
|
340
|
+
* promotes the floating widget without losing scrollback or the running
|
|
341
|
+
* shell. We move the references over, then rebind every handler that
|
|
342
|
+
* captured the source state in its closure (onData/onResize/keyboard
|
|
343
|
+
* handler/WebSocket callbacks) to point at the new target state.
|
|
344
|
+
*/
|
|
345
|
+
function transferFloatingToTab(sourceState, targetState) {
|
|
346
|
+
targetState.terminal = sourceState.terminal;
|
|
347
|
+
targetState.fitAddon = sourceState.fitAddon;
|
|
348
|
+
targetState.ws = sourceState.ws;
|
|
349
|
+
targetState.connected = sourceState.connected;
|
|
350
|
+
targetState.initialized = sourceState.initialized;
|
|
351
|
+
targetState.cwd = sourceState.cwd;
|
|
352
|
+
targetState.liveCwd = sourceState.liveCwd;
|
|
353
|
+
targetState.sessionId = sourceState.sessionId;
|
|
354
|
+
targetState.status = sourceState.status;
|
|
355
|
+
|
|
356
|
+
// Create new file path provider for this tab (getCwd closure must reference targetState)
|
|
357
|
+
const newFilePathProvider = new FilePathLinkProvider(
|
|
358
|
+
targetState.terminal,
|
|
359
|
+
() => targetState.liveCwd || targetState.cwd,
|
|
360
|
+
makeLiveCwdRefresher(targetState)
|
|
361
|
+
);
|
|
362
|
+
targetState.terminal.registerLinkProvider(newFilePathProvider);
|
|
363
|
+
targetState.filePathProvider = newFilePathProvider;
|
|
364
|
+
|
|
365
|
+
// Clear source session state (don't dispose - tab owns it now)
|
|
366
|
+
sourceState.terminal = null;
|
|
367
|
+
sourceState.fitAddon = null;
|
|
368
|
+
sourceState.filePathProvider = null;
|
|
369
|
+
sourceState.ws = null;
|
|
370
|
+
sourceState.connected = false;
|
|
371
|
+
sourceState.initialized = false;
|
|
372
|
+
sourceState.sessionId = null;
|
|
373
|
+
|
|
374
|
+
// IMPORTANT: Rebind ALL handlers to use targetState (the new tab).
|
|
375
|
+
// The original handlers captured sourceState in their closure.
|
|
376
|
+
// Old onData/onResize still fire but sourceState.ws is null so they're no-ops.
|
|
377
|
+
|
|
378
|
+
// Rebind terminal input with keyboard bar modifier support
|
|
379
|
+
targetState.terminal.onData(data => {
|
|
380
|
+
if (targetState.ws && targetState.ws.readyState === WebSocket.OPEN) {
|
|
381
|
+
if (targetState.keyboardBar?.hasActiveModifiers()) {
|
|
382
|
+
const mods = targetState.keyboardBar.consumeModifiers();
|
|
383
|
+
let modified = data;
|
|
384
|
+
if (mods.ctrl && data.length === 1) {
|
|
385
|
+
const code = data.toLowerCase().charCodeAt(0);
|
|
386
|
+
if (code >= 97 && code <= 122) {
|
|
387
|
+
modified = String.fromCharCode(code - 96);
|
|
388
|
+
}
|
|
389
|
+
} else if (mods.alt) {
|
|
390
|
+
modified = '\x1b' + data;
|
|
391
|
+
}
|
|
392
|
+
targetState.ws.send(modified);
|
|
393
|
+
} else {
|
|
394
|
+
targetState.ws.send(data);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
targetState.terminal.onResize(({ rows, cols }) => {
|
|
399
|
+
if (targetState.ws && targetState.ws.readyState === WebSocket.OPEN) {
|
|
400
|
+
targetState.ws.send(JSON.stringify({ type: 'resize', rows, cols }));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Rebind attachCustomKeyEventHandler to reference targetState's keyboard bar
|
|
405
|
+
targetState.terminal.attachCustomKeyEventHandler((e) => {
|
|
406
|
+
if (e.type !== 'keydown') return true;
|
|
407
|
+
|
|
408
|
+
// Virtual modifier keys from keyboard bar
|
|
409
|
+
if (targetState.keyboardBar?.hasActiveModifiers()) {
|
|
410
|
+
const ch = e.key;
|
|
411
|
+
if (ch.length === 1) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
const mods = targetState.keyboardBar.consumeModifiers();
|
|
414
|
+
let data = ch;
|
|
415
|
+
if (mods.ctrl) {
|
|
416
|
+
const code = ch.toLowerCase().charCodeAt(0);
|
|
417
|
+
if (code >= 97 && code <= 122) {
|
|
418
|
+
data = String.fromCharCode(code - 96);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (mods.alt) {
|
|
422
|
+
data = '\x1b' + data;
|
|
423
|
+
}
|
|
424
|
+
if (targetState.ws?.readyState === WebSocket.OPEN) {
|
|
425
|
+
targetState.ws.send(data);
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const key = e.key.toLowerCase();
|
|
432
|
+
if (e.shiftKey || e.altKey) return true;
|
|
433
|
+
|
|
434
|
+
if (key === 'c' && e.metaKey && !e.ctrlKey) {
|
|
435
|
+
const selection = getUnwrappedTerminalSelection(targetState.terminal);
|
|
436
|
+
debug.log('[Terminal/Tab] Cmd+C handler: selection =', JSON.stringify(selection?.substring(0, 80)));
|
|
437
|
+
if (selection) {
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
copyToClipboard(selection);
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (key === 'c' && e.ctrlKey && !e.metaKey) {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
if (targetState.ws?.readyState === WebSocket.OPEN) {
|
|
447
|
+
targetState.ws.send('\x03');
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (key === 'v' && e.metaKey && !e.ctrlKey) {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
navigator.clipboard.readText().then(text => {
|
|
455
|
+
if (text && targetState.ws?.readyState === WebSocket.OPEN) {
|
|
456
|
+
if (targetState.bracketedPasteMode) {
|
|
457
|
+
targetState.ws.send('\x1b[200~' + text + '\x1b[201~');
|
|
458
|
+
} else {
|
|
459
|
+
targetState.ws.send(text);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}).catch(() => {});
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Cmd+N / Ctrl+N → let bubble to shortcut handler (new draft)
|
|
467
|
+
if (key === 'n' && (e.metaKey || e.ctrlKey)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// CRITICAL: Rebind WebSocket handlers to use targetState
|
|
475
|
+
targetState.ws.onmessage = (event) => {
|
|
476
|
+
targetState.lastMessageAt = Date.now();
|
|
477
|
+
const data = event.data;
|
|
478
|
+
if (data.startsWith('{')) {
|
|
479
|
+
try {
|
|
480
|
+
const msg = JSON.parse(data);
|
|
481
|
+
handleJsonMessage(msg, targetState);
|
|
482
|
+
return;
|
|
483
|
+
} catch (e) {}
|
|
484
|
+
}
|
|
485
|
+
if (targetState.terminal) {
|
|
486
|
+
if (targetState.loadingEl) {
|
|
487
|
+
targetState.loadingEl.hidden = true;
|
|
488
|
+
}
|
|
489
|
+
// Track bracketed paste mode from shell output
|
|
490
|
+
if (data.includes('\x1b[?2004h')) targetState.bracketedPasteMode = true;
|
|
491
|
+
if (data.includes('\x1b[?2004l')) targetState.bracketedPasteMode = false;
|
|
492
|
+
targetState.terminal.write(data);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
targetState.ws.onclose = (event) => {
|
|
496
|
+
debug.log('Terminal WebSocket closed:', event.code, event.reason);
|
|
497
|
+
targetState.connected = false;
|
|
498
|
+
updateStatus('disconnected', targetState);
|
|
499
|
+
};
|
|
500
|
+
targetState.ws.onerror = (error) => {
|
|
501
|
+
console.warn('Terminal WebSocket error (connection lost):', error);
|
|
502
|
+
updateStatus('error', targetState);
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
507
|
+
// Public API
|
|
508
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
export const TerminalWidget = {
|
|
511
|
+
/**
|
|
512
|
+
* Write text to current session's terminal
|
|
513
|
+
*/
|
|
514
|
+
write(text) {
|
|
515
|
+
const sState = getSessionState();
|
|
516
|
+
if (sState.terminal) {
|
|
517
|
+
sState.terminal.write(text);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Send key to current session's terminal (for keyboard bar)
|
|
523
|
+
*/
|
|
524
|
+
sendKey(key, modifiers = {}) {
|
|
525
|
+
const sState = getSessionState();
|
|
526
|
+
if (!sState.ws || sState.ws.readyState !== WebSocket.OPEN) return;
|
|
527
|
+
|
|
528
|
+
const keyMap = {
|
|
529
|
+
'Escape': '\x1b',
|
|
530
|
+
'Tab': '\t',
|
|
531
|
+
'ArrowUp': '\x1b[A',
|
|
532
|
+
'ArrowDown': '\x1b[B',
|
|
533
|
+
'ArrowRight': '\x1b[C',
|
|
534
|
+
'ArrowLeft': '\x1b[D',
|
|
535
|
+
'Home': '\x1b[H',
|
|
536
|
+
'End': '\x1b[F',
|
|
537
|
+
'PageUp': '\x1b[5~',
|
|
538
|
+
'PageDown': '\x1b[6~',
|
|
539
|
+
'Delete': '\x1b[3~',
|
|
540
|
+
'Backspace': '\x7f',
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
let data = keyMap[key] || key;
|
|
544
|
+
|
|
545
|
+
// Apply modifiers
|
|
546
|
+
if (modifiers.ctrl && data.length === 1) {
|
|
547
|
+
const code = data.toLowerCase().charCodeAt(0);
|
|
548
|
+
if (code >= 97 && code <= 122) {
|
|
549
|
+
data = String.fromCharCode(code - 96);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
sState.ws.send(data);
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Clear current session's terminal
|
|
558
|
+
*/
|
|
559
|
+
clear() {
|
|
560
|
+
const sState = getSessionState();
|
|
561
|
+
if (sState.terminal) {
|
|
562
|
+
sState.terminal.clear();
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Focus current session's terminal
|
|
568
|
+
*/
|
|
569
|
+
focus() {
|
|
570
|
+
const sState = getSessionState();
|
|
571
|
+
if (sState.terminal) {
|
|
572
|
+
sState.terminal.focus();
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Paste text from clipboard into the active terminal.
|
|
578
|
+
* Returns true if paste was handled, false if no terminal available.
|
|
579
|
+
*/
|
|
580
|
+
async paste() {
|
|
581
|
+
// Find the right terminal state: last focused, or active tab, or session floating
|
|
582
|
+
let target = lastFocusedIfRecent();
|
|
583
|
+
if (!target) {
|
|
584
|
+
// Check active widget tab
|
|
585
|
+
const activeTabId = window.app?.tabCtrl?.activeWidgetTabId;
|
|
586
|
+
if (activeTabId) {
|
|
587
|
+
const tabState = tabStates.get(activeTabId);
|
|
588
|
+
if (tabState?.ws?.readyState === WebSocket.OPEN) {
|
|
589
|
+
target = tabState;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!target) {
|
|
594
|
+
// Fall back to session floating terminal
|
|
595
|
+
const sState = getSessionState();
|
|
596
|
+
if (sState.ws?.readyState === WebSocket.OPEN) {
|
|
597
|
+
target = sState;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!target) return false;
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const text = await navigator.clipboard.readText();
|
|
604
|
+
if (!text) return false;
|
|
605
|
+
if (target.bracketedPasteMode) {
|
|
606
|
+
target.ws.send('\x1b[200~' + text + '\x1b[201~');
|
|
607
|
+
} else {
|
|
608
|
+
target.ws.send(text);
|
|
609
|
+
}
|
|
610
|
+
target.terminal?.focus();
|
|
611
|
+
return true;
|
|
612
|
+
} catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Returns true if a terminal was recently focused (for paste routing)
|
|
619
|
+
*/
|
|
620
|
+
isRecentlyFocused() {
|
|
621
|
+
if (lastFocusedIfRecent()) return true;
|
|
622
|
+
// Also check if active tab is a terminal
|
|
623
|
+
const activeTabId = window.app?.tabCtrl?.activeWidgetTabId;
|
|
624
|
+
if (activeTabId) {
|
|
625
|
+
const tab = window.app?.tabCtrl?.widgetTabs?.find(t => t.id === activeTabId);
|
|
626
|
+
if (tab && (tab.widgetId === 'terminal' || tab.isTerminal)) {
|
|
627
|
+
const tabState = tabStates.get(activeTabId);
|
|
628
|
+
if (tabState?.ws?.readyState === WebSocket.OPEN) return true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Set font size (updates all session terminals and tab terminals)
|
|
636
|
+
*/
|
|
637
|
+
setFontSize(fontSize) {
|
|
638
|
+
for (const [, sState] of sessionStates) {
|
|
639
|
+
if (sState.terminal) {
|
|
640
|
+
sState.terminal.options.fontSize = fontSize;
|
|
641
|
+
fitTerminal(sState);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// Update all tab terminals
|
|
645
|
+
for (const [, tabState] of tabStates) {
|
|
646
|
+
if (tabState.terminal) {
|
|
647
|
+
tabState.terminal.options.fontSize = fontSize;
|
|
648
|
+
fitTerminal(tabState);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Fit terminal to container
|
|
655
|
+
*/
|
|
656
|
+
fit: () => {
|
|
657
|
+
// Fit current session's terminal
|
|
658
|
+
const sState = getSessionState();
|
|
659
|
+
fitTerminal(sState);
|
|
660
|
+
// Fit all tab terminals
|
|
661
|
+
for (const [, tabState] of tabStates) {
|
|
662
|
+
fitTerminal(tabState);
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Get current state (for debugging)
|
|
668
|
+
*/
|
|
669
|
+
getState: () => {
|
|
670
|
+
const sessions = {};
|
|
671
|
+
for (const [sid, sState] of sessionStates) {
|
|
672
|
+
sessions[sid] = {
|
|
673
|
+
connected: sState.connected,
|
|
674
|
+
status: sState.status,
|
|
675
|
+
cwd: sState.cwd,
|
|
676
|
+
sessionId: sState.sessionId,
|
|
677
|
+
initialized: sState.initialized,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const tabs = {};
|
|
681
|
+
for (const [tabId, tabState] of tabStates) {
|
|
682
|
+
tabs[tabId] = {
|
|
683
|
+
connected: tabState.connected,
|
|
684
|
+
status: tabState.status,
|
|
685
|
+
cwd: tabState.cwd,
|
|
686
|
+
sessionId: tabState.sessionId,
|
|
687
|
+
initialized: tabState.initialized,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
sessions,
|
|
692
|
+
sessionCount: sessionStates.size,
|
|
693
|
+
tabs,
|
|
694
|
+
tabCount: tabStates.size
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Check if current session's terminal is connected
|
|
700
|
+
*/
|
|
701
|
+
isConnected: () => getSessionState().connected,
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get WebSocket (for keyboard bar routing - prefers current session, falls back to first tab with connection)
|
|
705
|
+
*/
|
|
706
|
+
getWebSocket: () => {
|
|
707
|
+
const sState = getSessionState();
|
|
708
|
+
if (sState.ws) return sState.ws;
|
|
709
|
+
for (const [, tabState] of tabStates) {
|
|
710
|
+
if (tabState.ws) return tabState.ws;
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Get session terminal state (for advanced use)
|
|
717
|
+
* @param {string} [sessionId] - Session ID (defaults to current)
|
|
718
|
+
*/
|
|
719
|
+
getSessionTerminalState: (sessionId) => getSessionState(sessionId),
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Get tab terminal state by ID (for advanced use)
|
|
723
|
+
*/
|
|
724
|
+
getTabState: (tabId) => tabId ? tabStates.get(tabId) : null,
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Remove and cleanup a tab terminal state
|
|
728
|
+
*/
|
|
729
|
+
removeTabState: (tabId) => removeTabState(tabId),
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Get all tab IDs
|
|
733
|
+
*/
|
|
734
|
+
getTabIds: () => Array.from(tabStates.keys()),
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Get next terminal tab number for naming (Terminal 1, Terminal 2, etc.)
|
|
738
|
+
*/
|
|
739
|
+
getNextTabNumber: () => nextTabNumber(),
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Check if transfer from current session's terminal is available
|
|
743
|
+
*/
|
|
744
|
+
hasFloatingTerminal: () => !!getSessionState().terminal,
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Get configured terminal size (for config panel)
|
|
748
|
+
*/
|
|
749
|
+
getConfiguredSize,
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Set configured terminal size
|
|
753
|
+
*/
|
|
754
|
+
setConfiguredSize,
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Reset configured terminal size to defaults
|
|
758
|
+
*/
|
|
759
|
+
resetConfiguredSize,
|
|
760
|
+
|
|
761
|
+
/** Default dimensions */
|
|
762
|
+
DEFAULT_WIDTH: DEFAULT_TERMINAL_WIDTH,
|
|
763
|
+
DEFAULT_HEIGHT: DEFAULT_TERMINAL_HEIGHT,
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Attach terminal to a specific PTY session (e.g., orphaned PTY)
|
|
767
|
+
* Opens the terminal widget and connects with the given sessionId
|
|
768
|
+
* @param {string} ptySessionId - The terminal session ID to attach to
|
|
769
|
+
* @param {string} cwd - Working directory (optional)
|
|
770
|
+
*/
|
|
771
|
+
attachToSession(ptySessionId, cwd = null) {
|
|
772
|
+
const sState = getSessionState();
|
|
773
|
+
// Set forced session ID (will be used by connect instead of generating new one)
|
|
774
|
+
sState.forceSessionId = ptySessionId;
|
|
775
|
+
if (cwd) {
|
|
776
|
+
sState.cwd = cwd;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Open the terminal widget using WidgetManager API
|
|
780
|
+
WidgetManager.open('terminal');
|
|
781
|
+
|
|
782
|
+
// If already initialized, reconnect with new session
|
|
783
|
+
// (if not initialized, the render function will call connect with forceSessionId)
|
|
784
|
+
if (sState.initialized) {
|
|
785
|
+
connect(sState);
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Destroy state for a session
|
|
791
|
+
*/
|
|
792
|
+
destroySessionState,
|
|
793
|
+
};
|