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,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xterm.js link providers — URL detection (handling wrapped lines) and
|
|
3
|
+
* file-path detection (with on-click resolution via /api/find-file).
|
|
4
|
+
* Plus the lazy `loadXterm()` loader for the vendored xterm bundle.
|
|
5
|
+
*
|
|
6
|
+
* Both providers are pure xterm-addon classes — they don't reach back
|
|
7
|
+
* into TerminalState. init.js wires them onto the terminal instance and
|
|
8
|
+
* hands the file provider a `getCwd` closure (plus an optional
|
|
9
|
+
* `getLiveCwd` refresher — see makeLiveCwdRefresher) so it stays in
|
|
10
|
+
* sync when the session's CWD changes.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is context-aware: bare filenames are resolved with the
|
|
13
|
+
* shell's live cwd and with directory hints scraped from the terminal
|
|
14
|
+
* lines above the click (e.g. the `ll docs-ai/readme/` command that
|
|
15
|
+
* produced a bare-filename listing) — see collectDirHints().
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { CONFIG } from '../../config.js';
|
|
19
|
+
import { showToast } from '../../context-menu.js';
|
|
20
|
+
import { openExternal } from '../../utils.js';
|
|
21
|
+
import {
|
|
22
|
+
buildPathPattern,
|
|
23
|
+
buildStandalonePattern,
|
|
24
|
+
buildUrlPattern,
|
|
25
|
+
parseLineInfo,
|
|
26
|
+
isValidStandaloneFile,
|
|
27
|
+
cleanUrlTrailingPunct
|
|
28
|
+
} from '../../linkify-utils.js';
|
|
29
|
+
import { isContinuationRow } from './wrap-utils.js';
|
|
30
|
+
import { isAbsolutePath } from '../../path-utils.js';
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
// xterm.js Loading
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export async function loadXterm() {
|
|
37
|
+
// Check if already loaded
|
|
38
|
+
if (window.Terminal && window.FitAddon) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load CSS
|
|
43
|
+
if (!document.querySelector('link[href*="xterm.css"]')) {
|
|
44
|
+
const link = document.createElement('link');
|
|
45
|
+
link.rel = 'stylesheet';
|
|
46
|
+
link.href = '/static/vendor/xterm.css';
|
|
47
|
+
document.head.appendChild(link);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load xterm.js
|
|
51
|
+
if (!window.Terminal) {
|
|
52
|
+
await loadScript('/static/vendor/xterm.js');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load fit addon
|
|
56
|
+
if (!window.FitAddon) {
|
|
57
|
+
await loadScript('/static/vendor/xterm-addon-fit.js');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadScript(src) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const script = document.createElement('script');
|
|
64
|
+
script.src = src;
|
|
65
|
+
script.onload = resolve;
|
|
66
|
+
script.onerror = reject;
|
|
67
|
+
document.head.appendChild(script);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a refresher that fetches the shell's live cwd for a terminal
|
|
73
|
+
* state (/api/terminal-cwd reads /proc/<pid>/cwd, so it tracks the
|
|
74
|
+
* user's `cd`s). The result is cached on `state.liveCwd` — deliberately
|
|
75
|
+
* NOT written to `state.cwd`: the WS session key is derived from cwd,
|
|
76
|
+
* so mutating it would re-key the PTY on reconnect instead of
|
|
77
|
+
* reattaching to the running shell.
|
|
78
|
+
*/
|
|
79
|
+
export function makeLiveCwdRefresher(state) {
|
|
80
|
+
return async () => {
|
|
81
|
+
if (!state.sessionId) return null;
|
|
82
|
+
try {
|
|
83
|
+
const url = `${CONFIG.API_BASE}/api/terminal-cwd?session=${encodeURIComponent(state.sessionId)}`;
|
|
84
|
+
const response = await fetch(url);
|
|
85
|
+
if (!response.ok) return null;
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
if (data.cwd) state.liveCwd = data.cwd;
|
|
88
|
+
return data.cwd || null;
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
96
|
+
// URL Link Provider — handles URLs that span wrapped terminal lines
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Custom link provider for URLs that span wrapped terminal lines.
|
|
101
|
+
* Replaces WebLinksAddon which only detects URLs within a single line.
|
|
102
|
+
*
|
|
103
|
+
* Row-continuation detection is shared with the copy paths — see
|
|
104
|
+
* wrap-utils.js `isContinuationRow()` for the three wrapping scenarios
|
|
105
|
+
* (terminal isWrapped, OSC 8 id spanning the boundary, shell-input
|
|
106
|
+
* full-width heuristic).
|
|
107
|
+
*/
|
|
108
|
+
export class UrlLinkProvider {
|
|
109
|
+
constructor(terminal) {
|
|
110
|
+
this.terminal = terminal;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
provideLinks(lineNumber, callback) {
|
|
114
|
+
const buffer = this.terminal.buffer.active;
|
|
115
|
+
const line = buffer.getLine(lineNumber - 1);
|
|
116
|
+
if (!line) {
|
|
117
|
+
callback(undefined);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lineTexts = [];
|
|
122
|
+
const lineNumbers = [];
|
|
123
|
+
let currentLineNum = lineNumber;
|
|
124
|
+
|
|
125
|
+
// Walk back to find the start of the logical line sequence.
|
|
126
|
+
// (isContinuationRow takes 0-based row indices; lineNumber is 1-based.)
|
|
127
|
+
while (currentLineNum > 1 && isContinuationRow(this.terminal, currentLineNum - 1)) {
|
|
128
|
+
currentLineNum--;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Collect all lines in this logical line sequence (always from start)
|
|
132
|
+
let currentLine = buffer.getLine(currentLineNum - 1);
|
|
133
|
+
while (currentLine) {
|
|
134
|
+
let text = currentLine.translateToString(false);
|
|
135
|
+
const joinsNext = isContinuationRow(this.terminal, currentLineNum);
|
|
136
|
+
const nextLine = joinsNext ? buffer.getLine(currentLineNum) : null;
|
|
137
|
+
if (nextLine && !nextLine.isWrapped) {
|
|
138
|
+
// TUI-emitted continuation (OSC 8 / heuristic join): the row
|
|
139
|
+
// may be shorter than the terminal width, and untrimmed text
|
|
140
|
+
// pads it with blank cells — which would inject spaces into
|
|
141
|
+
// the joined URL. Strip the padding; a URL never ends in
|
|
142
|
+
// whitespace, so nothing legitimate is lost.
|
|
143
|
+
text = text.replace(/\s+$/, '');
|
|
144
|
+
}
|
|
145
|
+
lineTexts.push(text);
|
|
146
|
+
lineNumbers.push(currentLineNum);
|
|
147
|
+
|
|
148
|
+
if (nextLine) {
|
|
149
|
+
currentLineNum++;
|
|
150
|
+
currentLine = nextLine;
|
|
151
|
+
} else {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const fullText = lineTexts.join('');
|
|
157
|
+
if (!fullText.trim()) {
|
|
158
|
+
callback(undefined);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Convert character offset in combined text to multi-line range
|
|
163
|
+
const offsetToRange = (startOffset, length) => {
|
|
164
|
+
const endOffset = startOffset + length;
|
|
165
|
+
let charCount = 0;
|
|
166
|
+
let startLine = lineNumbers[0], startCol = startOffset + 1;
|
|
167
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
168
|
+
const lineLen = lineTexts[i].length;
|
|
169
|
+
if (charCount + lineLen > startOffset) {
|
|
170
|
+
startLine = lineNumbers[i];
|
|
171
|
+
startCol = (startOffset - charCount) + 1;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
charCount += lineLen;
|
|
175
|
+
}
|
|
176
|
+
charCount = 0;
|
|
177
|
+
let endLine = lineNumbers[lineNumbers.length - 1], endCol = endOffset;
|
|
178
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
179
|
+
const lineLen = lineTexts[i].length;
|
|
180
|
+
if (charCount + lineLen >= endOffset) {
|
|
181
|
+
endLine = lineNumbers[i];
|
|
182
|
+
endCol = (endOffset - charCount);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
charCount += lineLen;
|
|
186
|
+
}
|
|
187
|
+
return { start: { x: startCol, y: startLine }, end: { x: endCol, y: endLine } };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const links = [];
|
|
191
|
+
const urlPattern = buildUrlPattern();
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = urlPattern.exec(fullText)) !== null) {
|
|
194
|
+
const { url } = cleanUrlTrailingPunct(match[0]);
|
|
195
|
+
links.push({
|
|
196
|
+
range: offsetToRange(match.index, url.length),
|
|
197
|
+
text: url,
|
|
198
|
+
activate: (event, linkText) => {
|
|
199
|
+
if (event.button === 2) return;
|
|
200
|
+
// Anchor-click, not window.open — the latter silently
|
|
201
|
+
// no-ops in the iPad standalone PWA (see openExternal)
|
|
202
|
+
openExternal(url);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
callback(links.length > 0 ? links : undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
212
|
+
// File Path Link Provider
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Custom link provider for file paths in terminal output.
|
|
217
|
+
* Uses xterm.js's ILinkProvider interface to detect and handle file path clicks.
|
|
218
|
+
*
|
|
219
|
+
* Detection happens on-demand (when xterm needs links for a line).
|
|
220
|
+
* Verification happens on-click (to avoid performance overhead).
|
|
221
|
+
*/
|
|
222
|
+
export class FilePathLinkProvider {
|
|
223
|
+
constructor(terminal, getCwd, getLiveCwd = null) {
|
|
224
|
+
this.terminal = terminal;
|
|
225
|
+
this.getCwd = getCwd; // Function to get current CWD (may change)
|
|
226
|
+
this.getLiveCwd = getLiveCwd; // Optional async refresher for the shell's live cwd
|
|
227
|
+
this.pathCache = new Map(); // Cache verified paths: path -> resolved
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Called by xterm.js when it needs links for a buffer line.
|
|
232
|
+
* @param {number} lineNumber - 1-based line number in buffer
|
|
233
|
+
* @param {function} callback - Callback with array of links or undefined
|
|
234
|
+
*/
|
|
235
|
+
provideLinks(lineNumber, callback) {
|
|
236
|
+
const buffer = this.terminal.buffer.active;
|
|
237
|
+
// xterm passes 1-based line numbers; buffer.getLine() uses 0-based
|
|
238
|
+
const line = buffer.getLine(lineNumber - 1);
|
|
239
|
+
if (!line) {
|
|
240
|
+
callback(undefined);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Get terminal width for wrapped line handling
|
|
245
|
+
|
|
246
|
+
// Collect the full sequence of wrapped lines starting from this line
|
|
247
|
+
// A line is "wrapped" if the next line has isWrapped=true
|
|
248
|
+
// NOTE: lineNumber/currentLineNum stay 1-based (for xterm range y-coordinates)
|
|
249
|
+
const lineTexts = [];
|
|
250
|
+
const lineNumbers = [];
|
|
251
|
+
let currentLineNum = lineNumber;
|
|
252
|
+
let currentLine = line;
|
|
253
|
+
|
|
254
|
+
// First, check if this line is a continuation (isWrapped) - if so, find the start
|
|
255
|
+
while (currentLineNum > 1) {
|
|
256
|
+
const checkLine = buffer.getLine(currentLineNum - 1);
|
|
257
|
+
if (checkLine && checkLine.isWrapped) {
|
|
258
|
+
currentLineNum--;
|
|
259
|
+
} else {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Now collect all lines in this wrapped sequence
|
|
265
|
+
const startLineNum = currentLineNum;
|
|
266
|
+
currentLine = buffer.getLine(currentLineNum - 1);
|
|
267
|
+
|
|
268
|
+
while (currentLine) {
|
|
269
|
+
lineTexts.push(currentLine.translateToString(false)); // false = don't trim right
|
|
270
|
+
lineNumbers.push(currentLineNum);
|
|
271
|
+
|
|
272
|
+
// Check if next line continues the wrap (0-based: currentLineNum+1-1 = currentLineNum)
|
|
273
|
+
const nextLine = buffer.getLine(currentLineNum);
|
|
274
|
+
if (nextLine && nextLine.isWrapped) {
|
|
275
|
+
currentLineNum++;
|
|
276
|
+
currentLine = nextLine;
|
|
277
|
+
} else {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Only return links when called for the START of a wrapped sequence
|
|
283
|
+
// This prevents duplicate link detection for continuation lines
|
|
284
|
+
if (lineNumber !== startLineNum) {
|
|
285
|
+
callback(undefined);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Combine all wrapped lines into single text
|
|
290
|
+
const fullText = lineTexts.join('');
|
|
291
|
+
if (!fullText.trim()) {
|
|
292
|
+
callback(undefined);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const links = [];
|
|
297
|
+
const seenRanges = new Set(); // Avoid duplicate links at same position
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Convert a character offset in the combined text to multi-line range
|
|
301
|
+
*/
|
|
302
|
+
const offsetToRange = (startOffset, length) => {
|
|
303
|
+
const endOffset = startOffset + length;
|
|
304
|
+
|
|
305
|
+
// Find start line and column
|
|
306
|
+
let charCount = 0;
|
|
307
|
+
let startLine = lineNumbers[0];
|
|
308
|
+
let startCol = startOffset + 1; // 1-based
|
|
309
|
+
|
|
310
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
311
|
+
const lineLen = lineTexts[i].length;
|
|
312
|
+
if (charCount + lineLen > startOffset) {
|
|
313
|
+
startLine = lineNumbers[i];
|
|
314
|
+
startCol = (startOffset - charCount) + 1;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
charCount += lineLen;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Find end line and column (endOffset is exclusive, xterm end.x is inclusive)
|
|
321
|
+
charCount = 0;
|
|
322
|
+
let endLine = lineNumbers[lineNumbers.length - 1];
|
|
323
|
+
let endCol = endOffset;
|
|
324
|
+
|
|
325
|
+
for (let i = 0; i < lineTexts.length; i++) {
|
|
326
|
+
const lineLen = lineTexts[i].length;
|
|
327
|
+
if (charCount + lineLen >= endOffset) {
|
|
328
|
+
endLine = lineNumbers[i];
|
|
329
|
+
endCol = (endOffset - charCount);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
charCount += lineLen;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
start: { x: startCol, y: startLine },
|
|
337
|
+
end: { x: endCol, y: endLine }
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Match paths with directories (e.g., src/app.js, ./config.yaml)
|
|
342
|
+
const pathPattern = buildPathPattern();
|
|
343
|
+
let match;
|
|
344
|
+
while ((match = pathPattern.exec(fullText)) !== null) {
|
|
345
|
+
const path = match[1];
|
|
346
|
+
const lineInfo = match[2] || '';
|
|
347
|
+
const fullMatch = match[0];
|
|
348
|
+
|
|
349
|
+
// Skip invalid patterns
|
|
350
|
+
if (!path.includes('/')) continue;
|
|
351
|
+
if (/^\d+\.\d+/.test(path)) continue; // Version numbers
|
|
352
|
+
if (/^\d+\/\d+/.test(path)) continue; // Fractions like 1/2
|
|
353
|
+
|
|
354
|
+
const rangeKey = `${match.index}-${match.index + fullMatch.length}`;
|
|
355
|
+
if (seenRanges.has(rangeKey)) continue;
|
|
356
|
+
seenRanges.add(rangeKey);
|
|
357
|
+
|
|
358
|
+
const range = offsetToRange(match.index, fullMatch.length);
|
|
359
|
+
links.push({
|
|
360
|
+
range,
|
|
361
|
+
text: fullMatch,
|
|
362
|
+
activate: (event, linkText) => this.handleClick(path, lineInfo, event, range.start.y)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Match standalone filenames (e.g., server.py, CLAUDE.md)
|
|
367
|
+
const standalonePattern = buildStandalonePattern();
|
|
368
|
+
while ((match = standalonePattern.exec(fullText)) !== null) {
|
|
369
|
+
const filename = match[1];
|
|
370
|
+
const lineInfo = match[2] || '';
|
|
371
|
+
const fullMatch = match[0];
|
|
372
|
+
|
|
373
|
+
// Get preceding text for context validation
|
|
374
|
+
const precedingText = fullText.slice(Math.max(0, match.index - 20), match.index);
|
|
375
|
+
|
|
376
|
+
// Validate the match
|
|
377
|
+
if (!isValidStandaloneFile(filename, precedingText)) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const rangeKey = `${match.index}-${match.index + fullMatch.length}`;
|
|
382
|
+
if (seenRanges.has(rangeKey)) continue;
|
|
383
|
+
seenRanges.add(rangeKey);
|
|
384
|
+
|
|
385
|
+
const range = offsetToRange(match.index, fullMatch.length);
|
|
386
|
+
links.push({
|
|
387
|
+
range,
|
|
388
|
+
text: fullMatch,
|
|
389
|
+
activate: (event, linkText) => this.handleClick(filename, lineInfo, event, range.start.y)
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
callback(links.length > 0 ? links : undefined);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Scan the terminal lines above a link for directory context — e.g.
|
|
398
|
+
* the `ll docs-ai/readme/` command whose output is a bare-filename
|
|
399
|
+
* listing, or an earlier `cd some/dir`. Returns candidate directories
|
|
400
|
+
* (nearest line first, deduped, capped) that the server tries before
|
|
401
|
+
* falling back to a project-wide search.
|
|
402
|
+
*
|
|
403
|
+
* @param {number} startLine - 1-based buffer line the link starts on
|
|
404
|
+
* @returns {string[]} directory hints, most relevant first
|
|
405
|
+
*/
|
|
406
|
+
collectDirHints(startLine, maxLines = 200, maxHints = 8) {
|
|
407
|
+
const buffer = this.terminal?.buffer?.active;
|
|
408
|
+
if (!buffer || !startLine) return [];
|
|
409
|
+
|
|
410
|
+
const hints = [];
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
const add = (dir) => {
|
|
413
|
+
dir = dir.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
414
|
+
if (!dir || dir === '.' || dir === '..' || dir === '~') return;
|
|
415
|
+
if (seen.has(dir)) return;
|
|
416
|
+
seen.add(dir);
|
|
417
|
+
hints.push(dir);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const pattern = buildPathPattern();
|
|
421
|
+
const lowest = Math.max(1, startLine - maxLines);
|
|
422
|
+
for (let y = startLine; y >= lowest && hints.length < maxHints; y--) {
|
|
423
|
+
const text = buffer.getLine(y - 1)?.translateToString(true);
|
|
424
|
+
if (!text || !text.includes('/')) continue;
|
|
425
|
+
|
|
426
|
+
pattern.lastIndex = 0;
|
|
427
|
+
let match;
|
|
428
|
+
while ((match = pattern.exec(text)) !== null && hints.length < maxHints) {
|
|
429
|
+
const token = match[1];
|
|
430
|
+
if (!token.includes('/')) continue;
|
|
431
|
+
if (token.endsWith('/')) {
|
|
432
|
+
add(token); // explicit directory (trailing slash)
|
|
433
|
+
} else {
|
|
434
|
+
const parent = token.slice(0, token.lastIndexOf('/'));
|
|
435
|
+
if (parent) add(parent); // parent dir of a path-like token
|
|
436
|
+
add(token); // token itself may be a dir (`ls docs-ai/readme`)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return hints;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Resolve a path string via /api/find-file, passing any context
|
|
445
|
+
* hints along. Returns { path, isDir } with the resolved absolute
|
|
446
|
+
* path, or null if not found / on error. Absolute and ~ paths go
|
|
447
|
+
* through the server too so they get verified and classified as
|
|
448
|
+
* file vs directory. Result is cached per (cwd, path, hints).
|
|
449
|
+
*/
|
|
450
|
+
async resolveFullPath(path, cwd, hints = []) {
|
|
451
|
+
if (!path) return null;
|
|
452
|
+
const isAbsolute = isAbsolutePath(path) || path.startsWith('~');
|
|
453
|
+
if (!cwd && !isAbsolute) return null;
|
|
454
|
+
|
|
455
|
+
const cacheKey = `${cwd}:${path}:${hints.join('|')}`;
|
|
456
|
+
let resolved = this.pathCache.get(cacheKey);
|
|
457
|
+
if (resolved !== undefined) return resolved;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const params = new URLSearchParams({ name: path });
|
|
461
|
+
if (cwd) params.set('cwd', cwd);
|
|
462
|
+
for (const h of hints) params.append('hint', h);
|
|
463
|
+
const response = await fetch(`${CONFIG.API_BASE}/api/find-file?${params}`);
|
|
464
|
+
const data = await response.json();
|
|
465
|
+
resolved = (data.found && data.path)
|
|
466
|
+
? { path: data.path, isDir: !!data.is_dir }
|
|
467
|
+
: null;
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error('Failed to resolve file path:', err);
|
|
470
|
+
resolved = null;
|
|
471
|
+
}
|
|
472
|
+
this.pathCache.set(cacheKey, resolved);
|
|
473
|
+
return resolved;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Handle click on a file path link.
|
|
478
|
+
* Refreshes the shell's live cwd, gathers directory hints from the
|
|
479
|
+
* surrounding terminal lines, verifies the path via the server API,
|
|
480
|
+
* then opens preview — or the file explorer when the path is a
|
|
481
|
+
* directory.
|
|
482
|
+
*/
|
|
483
|
+
async handleClick(path, lineInfo, event, lineY = null) {
|
|
484
|
+
// Don't open preview on right-click (context menu handles it)
|
|
485
|
+
if (event && event.button === 2) return;
|
|
486
|
+
|
|
487
|
+
let cwd = this.getCwd();
|
|
488
|
+
if (this.getLiveCwd) {
|
|
489
|
+
const live = await this.getLiveCwd();
|
|
490
|
+
if (live) cwd = live;
|
|
491
|
+
}
|
|
492
|
+
const isAbsolute = isAbsolutePath(path) || path.startsWith('~');
|
|
493
|
+
if (!cwd && !isAbsolute) {
|
|
494
|
+
showToast('No working directory set', 'warning');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const hints = lineY ? this.collectDirHints(lineY) : [];
|
|
500
|
+
const resolved = await this.resolveFullPath(path, cwd, hints);
|
|
501
|
+
if (resolved) {
|
|
502
|
+
// Directories open in the file explorer, not the preview
|
|
503
|
+
if (resolved.isDir) {
|
|
504
|
+
window.FileExplorerWidget?.navigateTo?.(resolved.path);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Parse line info for preview options
|
|
509
|
+
const opts = parseLineInfo(lineInfo) || {};
|
|
510
|
+
|
|
511
|
+
// Ctrl/Cmd+click opens in editor, normal click opens preview
|
|
512
|
+
if (event.metaKey || event.ctrlKey) {
|
|
513
|
+
window.app?.openFileInEditor?.(resolved.path);
|
|
514
|
+
} else {
|
|
515
|
+
window.app?.previewFile?.(resolved.path, opts);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
showToast(`File not found: ${path}`, 'error');
|
|
519
|
+
}
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error('Failed to verify file path:', err);
|
|
522
|
+
showToast(`Error verifying path: ${path}`, 'error');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Clear the path cache (e.g., when CWD changes)
|
|
528
|
+
*/
|
|
529
|
+
clearCache() {
|
|
530
|
+
this.pathCache.clear();
|
|
531
|
+
}
|
|
532
|
+
}
|