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,373 @@
|
|
|
1
|
+
import S from './strings.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orphaned Terminals Manager
|
|
4
|
+
*
|
|
5
|
+
* Handles detection, display, and reconnection of orphaned terminal sessions
|
|
6
|
+
* (PTY processes running on server without active WebSocket connections)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CONFIG, debug } from './config.js';
|
|
10
|
+
import { TerminalWidget } from './widget-system/init.js';
|
|
11
|
+
import { escapeHtml, appConfirm } from './utils.js';
|
|
12
|
+
import { showToast } from './context-menu.js';
|
|
13
|
+
|
|
14
|
+
class OrphanTerminalsManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.btn = null;
|
|
17
|
+
this.badge = null;
|
|
18
|
+
this.dropdown = null;
|
|
19
|
+
this.list = null;
|
|
20
|
+
this.bulkActions = null;
|
|
21
|
+
this.orphans = [];
|
|
22
|
+
this.isOpen = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the orphan terminals UI
|
|
27
|
+
*/
|
|
28
|
+
init() {
|
|
29
|
+
this.btn = document.getElementById('orphan-terminals-btn');
|
|
30
|
+
this.badge = document.getElementById('orphan-badge');
|
|
31
|
+
this.dropdown = document.getElementById('orphan-dropdown');
|
|
32
|
+
this.list = document.getElementById('orphan-list');
|
|
33
|
+
this.bulkActions = document.getElementById('orphan-bulk-actions');
|
|
34
|
+
const refreshBtn = document.getElementById('orphan-refresh-btn');
|
|
35
|
+
const tabAllBtn = document.getElementById('orphan-tab-all-btn');
|
|
36
|
+
const killAllBtn = document.getElementById('orphan-kill-all-btn');
|
|
37
|
+
|
|
38
|
+
if (!this.btn || !this.dropdown) {
|
|
39
|
+
console.warn('Orphan terminals UI elements not found');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Toggle dropdown on button click
|
|
44
|
+
this.btn.addEventListener('click', (e) => {
|
|
45
|
+
// Don't toggle if clicking inside dropdown
|
|
46
|
+
if (e.target.closest('.orphan-terminals-dropdown')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.toggle();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Refresh button
|
|
53
|
+
if (refreshBtn) {
|
|
54
|
+
refreshBtn.addEventListener('click', (e) => {
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
this.refresh();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Tab All button
|
|
61
|
+
if (tabAllBtn) {
|
|
62
|
+
tabAllBtn.addEventListener('click', (e) => {
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
this.tabAll();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Kill All button
|
|
69
|
+
if (killAllBtn) {
|
|
70
|
+
killAllBtn.addEventListener('click', (e) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
this.killAll();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Close on outside click
|
|
77
|
+
document.addEventListener('click', (e) => {
|
|
78
|
+
if (this.isOpen && !this.btn.contains(e.target)) {
|
|
79
|
+
this.close();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Close on Escape
|
|
84
|
+
document.addEventListener('keydown', (e) => {
|
|
85
|
+
if (e.key === 'Escape' && this.isOpen) {
|
|
86
|
+
this.close();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Initial check
|
|
91
|
+
this.refresh();
|
|
92
|
+
|
|
93
|
+
// Periodic refresh every 30 seconds
|
|
94
|
+
setInterval(() => this.refreshBadge(), 30000);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Toggle dropdown visibility
|
|
99
|
+
*/
|
|
100
|
+
toggle() {
|
|
101
|
+
if (this.isOpen) {
|
|
102
|
+
this.close();
|
|
103
|
+
} else {
|
|
104
|
+
this.open();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Open the dropdown
|
|
110
|
+
*/
|
|
111
|
+
open() {
|
|
112
|
+
this.isOpen = true;
|
|
113
|
+
this.dropdown.classList.add('visible');
|
|
114
|
+
this.refresh();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Close the dropdown
|
|
119
|
+
*/
|
|
120
|
+
close() {
|
|
121
|
+
this.isOpen = false;
|
|
122
|
+
this.dropdown.classList.remove('visible');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Refresh the orphaned terminals list
|
|
127
|
+
*/
|
|
128
|
+
async refresh() {
|
|
129
|
+
try {
|
|
130
|
+
const orphans = await this.fetchOrphans();
|
|
131
|
+
this.orphans = orphans;
|
|
132
|
+
this.updateBadge();
|
|
133
|
+
this.renderList();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('Failed to fetch orphaned terminals:', err);
|
|
136
|
+
this.list.innerHTML = `<div class="orphan-dropdown-empty">${S.orphan_terminals.failed_load}</div>`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Just refresh the badge count (lightweight)
|
|
142
|
+
*/
|
|
143
|
+
async refreshBadge() {
|
|
144
|
+
try {
|
|
145
|
+
const orphans = await this.fetchOrphans();
|
|
146
|
+
this.orphans = orphans;
|
|
147
|
+
this.updateBadge();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Silently fail for background refresh
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fetch orphaned terminals from server
|
|
155
|
+
* @returns {Promise<Array>} List of orphaned terminal sessions
|
|
156
|
+
*/
|
|
157
|
+
async fetchOrphans() {
|
|
158
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/terminals`);
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`HTTP ${response.status}`);
|
|
161
|
+
}
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
const serverTerminals = data.terminals || [];
|
|
164
|
+
|
|
165
|
+
// Get currently connected terminal session IDs
|
|
166
|
+
const connectedIds = this.getConnectedSessionIds();
|
|
167
|
+
|
|
168
|
+
// Filter to only orphaned ones (not connected by any tab)
|
|
169
|
+
return serverTerminals.filter(t => !connectedIds.has(t.session));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get session IDs of all currently connected terminals
|
|
174
|
+
* @returns {Set<string>}
|
|
175
|
+
*/
|
|
176
|
+
getConnectedSessionIds() {
|
|
177
|
+
const ids = new Set();
|
|
178
|
+
|
|
179
|
+
// Get tab terminal session IDs
|
|
180
|
+
const tabIds = TerminalWidget.getTabIds();
|
|
181
|
+
for (const tabId of tabIds) {
|
|
182
|
+
const state = TerminalWidget.getTabState(tabId);
|
|
183
|
+
if (state?.sessionId) {
|
|
184
|
+
ids.add(state.sessionId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get floating terminal session ID if exists
|
|
189
|
+
// Note: Floating terminal uses a different state access pattern
|
|
190
|
+
// We'll check via the floating state export if available
|
|
191
|
+
|
|
192
|
+
return ids;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Update the badge count and bulk actions visibility
|
|
197
|
+
*/
|
|
198
|
+
updateBadge() {
|
|
199
|
+
const count = this.orphans.filter(t => t.alive).length;
|
|
200
|
+
if (this.badge) {
|
|
201
|
+
this.badge.textContent = count > 0 ? count : '';
|
|
202
|
+
this.badge.dataset.count = count;
|
|
203
|
+
}
|
|
204
|
+
// Show bulk actions only when there are orphans
|
|
205
|
+
if (this.bulkActions) {
|
|
206
|
+
this.bulkActions.style.display = this.orphans.length > 0 ? 'flex' : 'none';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Render the orphaned terminals list
|
|
212
|
+
*/
|
|
213
|
+
renderList() {
|
|
214
|
+
if (!this.list) return;
|
|
215
|
+
|
|
216
|
+
if (this.orphans.length === 0) {
|
|
217
|
+
this.list.innerHTML = `<div class="orphan-dropdown-empty">${S.orphan_terminals.empty}</div>`;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.list.innerHTML = this.orphans.map(term => `
|
|
222
|
+
<div class="orphan-terminal-item" data-session="${escapeHtml(term.session)}">
|
|
223
|
+
<div class="orphan-terminal-info">
|
|
224
|
+
<span class="orphan-terminal-pid">PID ${term.pid || '?'}</span>
|
|
225
|
+
<span class="orphan-terminal-status ${term.alive ? '' : 'dead'}">
|
|
226
|
+
${term.alive ? 'running' : 'dead'}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="orphan-terminal-cwd" data-tooltip="${escapeHtml(term.cwd || '')}">${escapeHtml(term.cwd || 'unknown')}</div>
|
|
230
|
+
<div class="orphan-terminal-actions">
|
|
231
|
+
${term.alive ? `
|
|
232
|
+
<button class="attach-btn" data-action="tab">Tab</button>
|
|
233
|
+
<button class="float-btn" data-action="float">Float</button>
|
|
234
|
+
` : ''}
|
|
235
|
+
<button class="kill-btn" data-action="kill">Kill</button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`).join('');
|
|
239
|
+
|
|
240
|
+
// Add click handlers
|
|
241
|
+
this.list.querySelectorAll('.orphan-terminal-item').forEach(item => {
|
|
242
|
+
item.querySelectorAll('button').forEach(btn => {
|
|
243
|
+
btn.addEventListener('click', (e) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
const action = btn.dataset.action;
|
|
246
|
+
const session = item.dataset.session;
|
|
247
|
+
if (action === 'tab') {
|
|
248
|
+
this.attachToTab(session);
|
|
249
|
+
} else if (action === 'float') {
|
|
250
|
+
this.attachToFloat(session);
|
|
251
|
+
} else if (action === 'kill') {
|
|
252
|
+
this.killTerminal(session);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Attach to an orphaned terminal by creating a new tab with its session ID
|
|
261
|
+
* @param {string} sessionId - The terminal session ID to attach to
|
|
262
|
+
*/
|
|
263
|
+
attachToTab(sessionId) {
|
|
264
|
+
const term = this.orphans.find(t => t.session === sessionId);
|
|
265
|
+
if (!term) return;
|
|
266
|
+
|
|
267
|
+
// Create a new terminal tab with this session ID
|
|
268
|
+
if (window.app?.tabCtrl) {
|
|
269
|
+
const tabId = window.app.tabCtrl.openTerminalWidgetTab({
|
|
270
|
+
terminalSessionId: sessionId,
|
|
271
|
+
cwd: term.cwd
|
|
272
|
+
});
|
|
273
|
+
debug.log(`Attached to orphaned terminal ${sessionId} as tab ${tabId}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.close();
|
|
277
|
+
this.refresh();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Attach to an orphaned terminal in the floating panel
|
|
282
|
+
* @param {string} sessionId - The terminal session ID to attach to
|
|
283
|
+
*/
|
|
284
|
+
attachToFloat(sessionId) {
|
|
285
|
+
const term = this.orphans.find(t => t.session === sessionId);
|
|
286
|
+
if (!term) return;
|
|
287
|
+
|
|
288
|
+
// Attach to floating terminal with this session ID
|
|
289
|
+
TerminalWidget.attachToSession(sessionId, term.cwd);
|
|
290
|
+
debug.log(`Attached to orphaned terminal ${sessionId} in floating panel`);
|
|
291
|
+
|
|
292
|
+
this.close();
|
|
293
|
+
this.refresh();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Kill an orphaned terminal
|
|
298
|
+
* @param {string} sessionId - The terminal session ID to kill
|
|
299
|
+
*/
|
|
300
|
+
async killTerminal(sessionId) {
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/terminal/${encodeURIComponent(sessionId)}`, {
|
|
303
|
+
method: 'DELETE'
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(`HTTP ${response.status}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
debug.log(`Killed orphaned terminal: ${sessionId}`);
|
|
311
|
+
this.refresh();
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('Failed to kill terminal:', err);
|
|
314
|
+
alert(S.orphan_terminals.kill_failed.replace('{error}', err.message));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Open all alive orphaned terminals in tabs
|
|
320
|
+
*/
|
|
321
|
+
tabAll() {
|
|
322
|
+
const aliveOrphans = this.orphans.filter(t => t.alive);
|
|
323
|
+
if (aliveOrphans.length === 0) return;
|
|
324
|
+
|
|
325
|
+
for (const term of aliveOrphans) {
|
|
326
|
+
if (window.app?.tabCtrl) {
|
|
327
|
+
window.app.tabCtrl.openTerminalWidgetTab({
|
|
328
|
+
terminalSessionId: term.session,
|
|
329
|
+
cwd: term.cwd
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
debug.log(`Opened ${aliveOrphans.length} orphaned terminals as tabs`);
|
|
335
|
+
this.close();
|
|
336
|
+
this.refresh();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Kill all orphaned terminals
|
|
341
|
+
*/
|
|
342
|
+
async killAll() {
|
|
343
|
+
if (this.orphans.length === 0) return;
|
|
344
|
+
|
|
345
|
+
const count = this.orphans.length;
|
|
346
|
+
const ok = await appConfirm(
|
|
347
|
+
S.orphan_terminals.kill_all_confirm.replace('{count}', count),
|
|
348
|
+
{ confirmLabel: 'Kill All', danger: true }
|
|
349
|
+
);
|
|
350
|
+
if (!ok) return;
|
|
351
|
+
|
|
352
|
+
const results = await Promise.allSettled(
|
|
353
|
+
this.orphans.map(term =>
|
|
354
|
+
fetch(`${CONFIG.API_BASE}/api/terminal/${encodeURIComponent(term.session)}`, {
|
|
355
|
+
method: 'DELETE'
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const succeeded = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
|
|
361
|
+
const failed = count - succeeded;
|
|
362
|
+
|
|
363
|
+
debug.log(`Killed ${succeeded} orphaned terminals${failed > 0 ? `, ${failed} failed` : ''}`);
|
|
364
|
+
showToast(failed > 0
|
|
365
|
+
? S.orphan_terminals.kill_all_partial
|
|
366
|
+
.replace('{ok}', succeeded).replace('{count}', count).replace('{failed}', failed)
|
|
367
|
+
: S.orphan_terminals.kill_all_done.replace('{count}', succeeded));
|
|
368
|
+
this.refresh();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Singleton instance
|
|
373
|
+
export const orphanTerminals = new OrphanTerminalsManager();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-flavored path helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every path the client manipulates belongs to the SERVER's filesystem,
|
|
5
|
+
* not the browser's — an iPad can be driving a Windows server — so the
|
|
6
|
+
* flavor comes from what the server reports (INSTANCE_CONFIG.path_style),
|
|
7
|
+
* never from navigator.platform.
|
|
8
|
+
*
|
|
9
|
+
* Before this module the resolver was POSIX-only: `startsWith('/')` meant
|
|
10
|
+
* "absolute" and joins were `'/' + parts.join('/')`, so a Windows cwd came
|
|
11
|
+
* back as `/C:/Users/me/proj` — a drive letter demoted to a path segment,
|
|
12
|
+
* which then 404s every listing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const WIN_ABS = /^[A-Za-z]:[\\/]/;
|
|
16
|
+
const UNC = /^\\\\[^\\/]+[\\/][^\\/]+/;
|
|
17
|
+
|
|
18
|
+
/** Windows path semantics? Falls back to sniffing a known server path so a
|
|
19
|
+
* cached page that predates the config flag still behaves. */
|
|
20
|
+
export function isWindowsPaths(sampleAbsPath = '') {
|
|
21
|
+
const declared = window.INSTANCE_CONFIG?.path_style;
|
|
22
|
+
if (declared === 'windows') return true;
|
|
23
|
+
if (declared === 'posix') return false;
|
|
24
|
+
return WIN_ABS.test(sampleAbsPath) || UNC.test(sampleAbsPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function pathSep(sample = '') {
|
|
28
|
+
return isWindowsPaths(sample) ? '\\' : '/';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Which separators the flavor ACCEPTS — a different question from which one
|
|
33
|
+
* `pathSep` emits, and the one this module used to get wrong. Windows treats
|
|
34
|
+
* '\' and '/' interchangeably, but on POSIX a backslash is an ordinary
|
|
35
|
+
* filename character: splitting on it truncates a real file called
|
|
36
|
+
* `notes\2024-Q1.md` down to `2024-Q1.md`.
|
|
37
|
+
*/
|
|
38
|
+
function splitRe(sample) { return isWindowsPaths(sample) ? /[\\/]+/ : /\/+/; }
|
|
39
|
+
function trailSepRe(sample) { return isWindowsPaths(sample) ? /[\\/]+$/ : /\/+$/; }
|
|
40
|
+
function leadSepRe(sample) { return isWindowsPaths(sample) ? /^[\\/]+/ : /^\/+/; }
|
|
41
|
+
|
|
42
|
+
/** Windows flavor for a pair of paths — either one can carry the tell (a
|
|
43
|
+
* drive letter or UNC prefix), and a declared path_style outranks both. */
|
|
44
|
+
function winPair(a, b) { return isWindowsPaths(a || '') || isWindowsPaths(b || ''); }
|
|
45
|
+
|
|
46
|
+
/** Does this path stand on its own (no cwd needed)? */
|
|
47
|
+
export function isAbsolutePath(p, sample = '') {
|
|
48
|
+
if (!p) return false;
|
|
49
|
+
if (isWindowsPaths(sample || p)) return WIN_ABS.test(p) || UNC.test(p);
|
|
50
|
+
return p.startsWith('/');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The leading part that must survive normalization: '/' on POSIX,
|
|
55
|
+
* 'C:\' or '\\server\share\' on Windows. '' for a relative path.
|
|
56
|
+
*/
|
|
57
|
+
export function pathRoot(p) {
|
|
58
|
+
if (!p) return '';
|
|
59
|
+
if (isWindowsPaths(p)) {
|
|
60
|
+
const unc = p.match(UNC);
|
|
61
|
+
if (unc) return unc[0] + '\\';
|
|
62
|
+
if (WIN_ABS.test(p)) return p.slice(0, 2) + '\\';
|
|
63
|
+
}
|
|
64
|
+
if (p.startsWith('/')) return '/';
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Split into components, dropping empties. `sample` exists for callers that
|
|
70
|
+
* hand us a rootless fragment — the drive letter that makes a path
|
|
71
|
+
* recognizably Windows has already been sliced off, so the fragment alone
|
|
72
|
+
* would sniff as POSIX.
|
|
73
|
+
*/
|
|
74
|
+
export function splitPath(p, sample = p) {
|
|
75
|
+
return (p || '').split(splitRe(sample)).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve `rel` against `base`, collapsing . and .. — the cross-platform
|
|
80
|
+
* replacement for `'/' + combined.split('/').filter(...).join('/')`.
|
|
81
|
+
*/
|
|
82
|
+
export function resolvePath(base, rel) {
|
|
83
|
+
const startFrom = isAbsolutePath(rel, base) ? rel : `${base || ''}/${rel || ''}`;
|
|
84
|
+
const root = pathRoot(startFrom);
|
|
85
|
+
const sep = isWindowsPaths(startFrom) ? '\\' : '/';
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const part of splitPath(root ? startFrom.slice(root.length) : startFrom, startFrom)) {
|
|
88
|
+
if (part === '.') continue;
|
|
89
|
+
if (part === '..') out.pop();
|
|
90
|
+
else out.push(part);
|
|
91
|
+
}
|
|
92
|
+
if (!root) return out.join(sep);
|
|
93
|
+
// root already ends with a separator; don't double it
|
|
94
|
+
return root + out.join(sep);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Last component, either separator. */
|
|
98
|
+
export function basename(p) {
|
|
99
|
+
const parts = splitPath(p);
|
|
100
|
+
return parts.length ? parts[parts.length - 1] : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Containing directory, or null at the root. */
|
|
104
|
+
export function parentOf(p) {
|
|
105
|
+
if (!p) return null;
|
|
106
|
+
const root = pathRoot(p);
|
|
107
|
+
const parts = splitPath(root ? p.slice(root.length) : p, p);
|
|
108
|
+
if (!parts.length) return null; // already at the root
|
|
109
|
+
parts.pop();
|
|
110
|
+
const sep = isWindowsPaths(p) ? '\\' : '/';
|
|
111
|
+
if (root) return parts.length ? root + parts.join(sep) : root;
|
|
112
|
+
return parts.join(sep) || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Join a directory and a child name with the server's separator. */
|
|
116
|
+
export function joinPath(dir, child) {
|
|
117
|
+
if (!dir) return child || '';
|
|
118
|
+
if (!child) return dir;
|
|
119
|
+
const sep = isWindowsPaths(dir) ? '\\' : '/';
|
|
120
|
+
return dir.replace(trailSepRe(dir), '') + sep + child;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Trailing separators removed, but never past the root. */
|
|
124
|
+
export function stripTrailingSep(p) {
|
|
125
|
+
if (!p) return p;
|
|
126
|
+
const root = pathRoot(p);
|
|
127
|
+
const trimmed = p.replace(trailSepRe(p), '');
|
|
128
|
+
return trimmed.length >= root.length && trimmed ? trimmed : (root || p);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `path` expressed relative to `base`, or `path` unchanged when it isn't
|
|
133
|
+
* under `base`. Replaces the `p.startsWith(cwd + '/') ? p.slice(cwd.length + 1) : p`
|
|
134
|
+
* idiom, which on Windows never matched — so shadow-git/history endpoints
|
|
135
|
+
* that require a repo-relative pathspec were handed absolute paths and
|
|
136
|
+
* silently returned nothing.
|
|
137
|
+
*
|
|
138
|
+
* Emits forward slashes on Windows: the consumers are git pathspecs and API
|
|
139
|
+
* keys, which are '/' on every platform. On POSIX the backslashes that
|
|
140
|
+
* survive are part of the filename, so they're left alone.
|
|
141
|
+
*/
|
|
142
|
+
export function relativeTo(path, base) {
|
|
143
|
+
if (!path || !base) return path;
|
|
144
|
+
if (!isUnder(path, base)) return path;
|
|
145
|
+
const win = winPair(path, base);
|
|
146
|
+
// stripTrailingSep is what isUnder compared against — including when
|
|
147
|
+
// base IS the root, where it stops at '/' or 'C:\' rather than emptying
|
|
148
|
+
// out. Slicing by any other length eats a real character.
|
|
149
|
+
const rest = path.slice(stripTrailingSep(base).length);
|
|
150
|
+
const trimmed = rest.replace(leadSepRe(base), '');
|
|
151
|
+
return win ? trimmed.replace(/\\/g, '/') : trimmed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Directory part of a path — '' when there's no separator. */
|
|
155
|
+
export function dirname(p) {
|
|
156
|
+
const parent = parentOf(p);
|
|
157
|
+
return parent === null ? '' : parent;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Is `child` inside `dir` (or equal to it)?
|
|
162
|
+
*
|
|
163
|
+
* On Windows this is case- and separator-insensitive, because the same file
|
|
164
|
+
* is reachable as `C:/Users/me/proj` or `C:\Users\me\proj` and the two sides
|
|
165
|
+
* often come from different places (the server reports one, a link the other).
|
|
166
|
+
* On POSIX neither folding applies — case matters and '\' is a filename
|
|
167
|
+
* character. Normalization is 1:1 in length apart from the trailing strip,
|
|
168
|
+
* so `relativeTo` can slice by `stripTrailingSep(dir).length`.
|
|
169
|
+
*/
|
|
170
|
+
export function isUnder(child, dir) {
|
|
171
|
+
if (!child || !dir) return false;
|
|
172
|
+
const win = winPair(child, dir);
|
|
173
|
+
const sep = win ? '\\' : '/';
|
|
174
|
+
const norm = (s) => (win ? s.toLowerCase().replace(/\//g, '\\') : s).replace(win ? /\\+$/ : /\/+$/, '');
|
|
175
|
+
const c = norm(child);
|
|
176
|
+
const d = norm(dir);
|
|
177
|
+
if (c === d) return true;
|
|
178
|
+
return c.startsWith(d) && c.charAt(d.length) === sep;
|
|
179
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance instrumentation module.
|
|
3
|
+
* Only loaded when ?perf=true is in the URL.
|
|
4
|
+
* Wraps key functions with performance.mark/measure calls.
|
|
5
|
+
*
|
|
6
|
+
* Usage from Playwright:
|
|
7
|
+
* window.__perfCollect() → returns all measures as array
|
|
8
|
+
* window.__perfClear() → clears all marks/measures
|
|
9
|
+
* window.__perfEnabled → true when loaded
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { debug } from './config.js';
|
|
13
|
+
|
|
14
|
+
window.__perfEnabled = true;
|
|
15
|
+
|
|
16
|
+
// ── Wrap a method with performance marks ──
|
|
17
|
+
function wrapMethod(obj, methodName, label) {
|
|
18
|
+
const original = obj[methodName];
|
|
19
|
+
if (!original) return;
|
|
20
|
+
obj[methodName] = function (...args) {
|
|
21
|
+
const markStart = `${label}-start`;
|
|
22
|
+
const markEnd = `${label}-end`;
|
|
23
|
+
performance.mark(markStart);
|
|
24
|
+
const result = original.apply(this, args);
|
|
25
|
+
performance.mark(markEnd);
|
|
26
|
+
performance.measure(label, markStart, markEnd);
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Instrument ChatController.renderMessages ──
|
|
32
|
+
function instrumentChatController() {
|
|
33
|
+
const chatCtrl = window.app?.chatCtrl;
|
|
34
|
+
if (!chatCtrl) return false;
|
|
35
|
+
wrapMethod(chatCtrl, 'renderMessages', 'render-messages');
|
|
36
|
+
wrapMethod(chatCtrl, '_renderMessagesIntoContainer', 'render-into-container');
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Instrument SessionContainerPool.activate ──
|
|
41
|
+
function instrumentContainerPool() {
|
|
42
|
+
const pool = window.app?.chatCtrl?._containerPool;
|
|
43
|
+
if (!pool) return false;
|
|
44
|
+
wrapMethod(pool, 'activate', 'pool-activate');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Instrument App._doSessionSwitch ──
|
|
49
|
+
function instrumentSessionSwitch() {
|
|
50
|
+
const app = window.app;
|
|
51
|
+
if (!app) return false;
|
|
52
|
+
wrapMethod(app, '_doSessionSwitch', 'session-switch');
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── API: Collect all measures ──
|
|
57
|
+
window.__perfCollect = function () {
|
|
58
|
+
return performance.getEntriesByType('measure').map(e => ({
|
|
59
|
+
name: e.name,
|
|
60
|
+
duration: Math.round(e.duration * 100) / 100,
|
|
61
|
+
startTime: Math.round(e.startTime * 100) / 100,
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── API: Clear all marks and measures ──
|
|
66
|
+
window.__perfClear = function () {
|
|
67
|
+
performance.clearMarks();
|
|
68
|
+
performance.clearMeasures();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ── API: Get measures by name ──
|
|
72
|
+
window.__perfGet = function (name) {
|
|
73
|
+
return performance.getEntriesByName(name, 'measure').map(e => ({
|
|
74
|
+
duration: Math.round(e.duration * 100) / 100,
|
|
75
|
+
startTime: Math.round(e.startTime * 100) / 100,
|
|
76
|
+
}));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── Apply all instrumentation ──
|
|
80
|
+
function applyInstrumentation() {
|
|
81
|
+
const results = {
|
|
82
|
+
chatController: instrumentChatController(),
|
|
83
|
+
containerPool: instrumentContainerPool(),
|
|
84
|
+
sessionSwitch: instrumentSessionSwitch(),
|
|
85
|
+
};
|
|
86
|
+
debug.log('[Perf] Instrumentation applied:', results);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// App may not be ready yet, wait for it
|
|
90
|
+
if (window.app?.chatCtrl) {
|
|
91
|
+
applyInstrumentation();
|
|
92
|
+
} else {
|
|
93
|
+
// Poll briefly for app readiness
|
|
94
|
+
let attempts = 0;
|
|
95
|
+
const interval = setInterval(() => {
|
|
96
|
+
if (window.app?.chatCtrl || ++attempts > 50) {
|
|
97
|
+
clearInterval(interval);
|
|
98
|
+
if (window.app?.chatCtrl) applyInstrumentation();
|
|
99
|
+
else console.warn('[Perf] App not ready after 5s, skipping instrumentation');
|
|
100
|
+
}
|
|
101
|
+
}, 100);
|
|
102
|
+
}
|