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,879 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenDialog — fish-style path picker for "Open" flows.
|
|
3
|
+
*
|
|
4
|
+
* The input is a literal path being typed. As the user types, a dim ghost
|
|
5
|
+
* suggestion extends inline showing the best prefix completion of the
|
|
6
|
+
* current segment. Tab or Right-arrow accepts the ghost and continues.
|
|
7
|
+
*
|
|
8
|
+
* Three input shapes, all prefix-based (no fuzzy scoring):
|
|
9
|
+
* - Empty → list cwd's contents
|
|
10
|
+
* - Plain text (no `/`) → prefix-match basenames across the project
|
|
11
|
+
* - Path-ish (has `/`, `~`, `.`) → strict directory listing + prefix filter
|
|
12
|
+
*
|
|
13
|
+
* Enter/click on a folder → drill into it (browse its contents). The
|
|
14
|
+
* auto-selected "Open this folder" row starts a session at the current dir.
|
|
15
|
+
* Enter on a file → preview (or new tab with Cmd/Ctrl+Enter).
|
|
16
|
+
*
|
|
17
|
+
* Works anywhere on the filesystem: `/etc/`, `~/Documents/`, `../`,
|
|
18
|
+
* absolute paths. OS file permissions still apply (the server can only
|
|
19
|
+
* read what its user can read); `/proc`, `/sys`, `/dev` are blocked.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import S from './strings.js';
|
|
23
|
+
import { CONFIG } from './config.js';
|
|
24
|
+
import { ICONS } from './widget-system/index.js';
|
|
25
|
+
import { escapeHtml, extractApiError } from './utils.js';
|
|
26
|
+
import { basename, dirname, isAbsolutePath, isUnder, joinPath, parentOf, pathRoot, pathSep, relativeTo, resolvePath, splitPath, stripTrailingSep } from './path-utils.js';
|
|
27
|
+
import { ContextMenu, copyToClipboard, showToast } from './context-menu.js';
|
|
28
|
+
|
|
29
|
+
const DEBOUNCE_MS = 80;
|
|
30
|
+
const PROJECT_CACHE_TTL = 60_000;
|
|
31
|
+
const PATH_CACHE_TTL = 5_000;
|
|
32
|
+
const RECENT_TTL = 30_000;
|
|
33
|
+
const MAX_LIST = 100;
|
|
34
|
+
const MAX_BASENAME_LIST = 50;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `absPath` under `base`, as text to put back in the input box.
|
|
38
|
+
*
|
|
39
|
+
* relativeTo() normalizes to '/' because its usual consumers are git
|
|
40
|
+
* pathspecs and API keys; here the separator has to match what the user
|
|
41
|
+
* types, so the server's own goes back in. Doing the offset by hand is what
|
|
42
|
+
* broke on a root base ('/', 'C:\'): `base.length + 1` ate the first real
|
|
43
|
+
* character, turning /etc/hosts into tc/hosts.
|
|
44
|
+
*/
|
|
45
|
+
function relDisplay(absPath, base) {
|
|
46
|
+
return relativeTo(absPath, base).split('/').join(pathSep(absPath));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class OpenDialogClass {
|
|
50
|
+
constructor() {
|
|
51
|
+
this._projectCache = new Map();
|
|
52
|
+
this._pathCache = new Map();
|
|
53
|
+
this._recentCache = new Map();
|
|
54
|
+
this._reqToken = 0;
|
|
55
|
+
this._items = [];
|
|
56
|
+
this._selectedIndex = 0;
|
|
57
|
+
this._ghost = '';
|
|
58
|
+
this._debounceTimer = null;
|
|
59
|
+
this._previousActiveEl = null;
|
|
60
|
+
this._ctxMenu = null;
|
|
61
|
+
this._built = false;
|
|
62
|
+
this._missingDir = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_build() {
|
|
66
|
+
if (this._built) return;
|
|
67
|
+
this._built = true;
|
|
68
|
+
|
|
69
|
+
const overlay = document.createElement('div');
|
|
70
|
+
overlay.id = 'open-dialog-overlay';
|
|
71
|
+
overlay.className = 'qs-overlay od-overlay';
|
|
72
|
+
overlay.setAttribute('role', 'dialog');
|
|
73
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
74
|
+
overlay.setAttribute('aria-label', S.open_dialog.aria_label);
|
|
75
|
+
overlay.hidden = true;
|
|
76
|
+
overlay.innerHTML = `
|
|
77
|
+
<div class="qs-modal od-modal" role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-owns="od-list">
|
|
78
|
+
<div class="qs-input-wrap od-input-wrap">
|
|
79
|
+
<span class="od-ghost" aria-hidden="true"
|
|
80
|
+
><span class="od-ghost-typed"></span
|
|
81
|
+
><span class="od-ghost-rest"></span
|
|
82
|
+
></span>
|
|
83
|
+
<input type="text" class="qs-input od-input" autocomplete="off" autocapitalize="off"
|
|
84
|
+
autocorrect="off" spellcheck="false"
|
|
85
|
+
data-shortcuts-disabled="true"
|
|
86
|
+
aria-controls="od-list"
|
|
87
|
+
placeholder="${escapeHtml(S.open_dialog.placeholder)}">
|
|
88
|
+
</div>
|
|
89
|
+
<div class="od-path" aria-live="polite"></div>
|
|
90
|
+
<div class="qs-list od-list" id="od-list" role="listbox"></div>
|
|
91
|
+
<div class="qs-footer od-footer">
|
|
92
|
+
<div class="qs-footer-row">
|
|
93
|
+
<span class="qs-hint">${S.open_dialog.hints.nav}</span>
|
|
94
|
+
<span class="qs-hint">${S.open_dialog.hints.parent}</span>
|
|
95
|
+
<span class="qs-hint od-hint-tab">${S.open_dialog.hints.complete}</span>
|
|
96
|
+
<span class="qs-hint od-hint-file">${S.open_dialog.hints.preview}</span>
|
|
97
|
+
<span class="qs-hint od-hint-dir-nav">${S.open_dialog.hints.enter_folder}</span>
|
|
98
|
+
<span class="qs-hint od-hint-dir-create">${S.open_dialog.hints.create_folder}</span>
|
|
99
|
+
<span class="qs-hint od-hint-dir-open">${S.open_dialog.hints.open_session}</span>
|
|
100
|
+
<span class="qs-hint">${S.open_dialog.hints.switcher}</span>
|
|
101
|
+
<span class="qs-hint">${S.open_dialog.hints.close}</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
document.body.appendChild(overlay);
|
|
107
|
+
|
|
108
|
+
this.overlay = overlay;
|
|
109
|
+
this.input = overlay.querySelector('.od-input');
|
|
110
|
+
this.list = overlay.querySelector('.od-list');
|
|
111
|
+
this.footer = overlay.querySelector('.od-footer');
|
|
112
|
+
this.ghostTyped = overlay.querySelector('.od-ghost-typed');
|
|
113
|
+
this.ghostRest = overlay.querySelector('.od-ghost-rest');
|
|
114
|
+
this.pathEl = overlay.querySelector('.od-path');
|
|
115
|
+
|
|
116
|
+
this.input.addEventListener('input', () => this._scheduleChange());
|
|
117
|
+
this.input.addEventListener('keydown', (e) => this._onKey(e));
|
|
118
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) this.hide(); });
|
|
119
|
+
|
|
120
|
+
this.list.addEventListener('click', (e) => {
|
|
121
|
+
const it = e.target.closest('.qs-item');
|
|
122
|
+
if (!it) return;
|
|
123
|
+
this._selectedIndex = parseInt(it.dataset.index, 10);
|
|
124
|
+
this._renderList();
|
|
125
|
+
this._submit(this._optsFromEvent(e));
|
|
126
|
+
// If the click drilled into a folder (dialog still open), pull focus
|
|
127
|
+
// back to the input so the user can keep typing / arrowing.
|
|
128
|
+
if (!this.overlay.hidden) this.input.focus();
|
|
129
|
+
});
|
|
130
|
+
this.list.addEventListener('contextmenu', (e) => {
|
|
131
|
+
const it = e.target.closest('.qs-item');
|
|
132
|
+
if (!it) return;
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
this._selectedIndex = parseInt(it.dataset.index, 10);
|
|
135
|
+
this._renderList();
|
|
136
|
+
this._showContextMenu(e.clientX, e.clientY);
|
|
137
|
+
});
|
|
138
|
+
// Swallow contextmenu on the overlay backdrop so the FAB radial doesn't fire.
|
|
139
|
+
overlay.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
show() {
|
|
143
|
+
this._build();
|
|
144
|
+
if (!this.overlay.hidden) {
|
|
145
|
+
this.input.focus();
|
|
146
|
+
this.input.select();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this._previousActiveEl = document.activeElement;
|
|
150
|
+
this.overlay.hidden = false;
|
|
151
|
+
this.input.value = '';
|
|
152
|
+
this._ghost = '';
|
|
153
|
+
this._items = [];
|
|
154
|
+
this._selectedIndex = 0;
|
|
155
|
+
const { mode, dir } = this._parse('');
|
|
156
|
+
this._renderCurrentPath(mode, dir);
|
|
157
|
+
this._renderGhost();
|
|
158
|
+
this._renderList();
|
|
159
|
+
requestAnimationFrame(() => {
|
|
160
|
+
this.input.focus();
|
|
161
|
+
this._refresh();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
hide() {
|
|
166
|
+
if (!this._built || this.overlay.hidden) return;
|
|
167
|
+
this.overlay.hidden = true;
|
|
168
|
+
clearTimeout(this._debounceTimer);
|
|
169
|
+
if (this._previousActiveEl && document.contains(this._previousActiveEl)) {
|
|
170
|
+
this._previousActiveEl.focus?.();
|
|
171
|
+
}
|
|
172
|
+
this._previousActiveEl = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
toggle() {
|
|
176
|
+
this._build();
|
|
177
|
+
if (this.overlay.hidden) this.show();
|
|
178
|
+
else this.hide();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
isOpen() {
|
|
182
|
+
return !!this._built && !this.overlay.hidden;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_scheduleChange() {
|
|
186
|
+
clearTimeout(this._debounceTimer);
|
|
187
|
+
this._debounceTimer = setTimeout(() => this._refresh(), DEBOUNCE_MS);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_onKey(e) {
|
|
191
|
+
switch (e.key) {
|
|
192
|
+
case 'Escape':
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
this.hide();
|
|
195
|
+
break;
|
|
196
|
+
case 'Backspace':
|
|
197
|
+
// Nothing left to delete → hand back to the fuzzy quick
|
|
198
|
+
// switcher (mirror of the switcher's `@` → Open dialog handoff).
|
|
199
|
+
// With text present, fall through to native character delete.
|
|
200
|
+
if (this.input.value === '') {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
this.hide();
|
|
203
|
+
window.QuickSwitcher?.show();
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'Enter':
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
if (this._items.length) this._submit(this._optsFromEvent(e));
|
|
209
|
+
break;
|
|
210
|
+
case 'ArrowDown':
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
this._move(1);
|
|
213
|
+
break;
|
|
214
|
+
case 'ArrowUp':
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
this._move(-1);
|
|
217
|
+
break;
|
|
218
|
+
case 'Tab':
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
// Tab: accept the ghost if present, otherwise cycle the list
|
|
221
|
+
// so the user can still navigate when nothing's ahead of them.
|
|
222
|
+
if (this._ghost) this._acceptGhost();
|
|
223
|
+
else this._move(e.shiftKey ? -1 : 1);
|
|
224
|
+
break;
|
|
225
|
+
case 'ArrowRight':
|
|
226
|
+
// Modifier held (shift/alt/ctrl/meta) → user is selecting or
|
|
227
|
+
// word-jumping; let the input handle it natively.
|
|
228
|
+
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
|
|
229
|
+
// Only intercept at end-of-input — otherwise the user is moving
|
|
230
|
+
// the caret inside the path and we shouldn't steal it.
|
|
231
|
+
// Priority: accept ghost → drill into selected dir.
|
|
232
|
+
if (this.input.selectionStart === this.input.value.length) {
|
|
233
|
+
if (this._ghost) {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
this._acceptGhost();
|
|
236
|
+
} else {
|
|
237
|
+
const item = this._items[this._selectedIndex];
|
|
238
|
+
// `is_open_here` is an action, not a deeper dir —
|
|
239
|
+
// skip it. `..` is fine (drill-in == go up).
|
|
240
|
+
if (item?.is_dir && !item.is_open_here) {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
this._drillInto(item);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case 'ArrowLeft': {
|
|
248
|
+
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
|
|
249
|
+
// Intercept when the caret is at position 0 (anywhere in the
|
|
250
|
+
// input) OR at the very end of a path that already ends with
|
|
251
|
+
// `/` (i.e. we're "in" a directory rather than editing a
|
|
252
|
+
// basename). Otherwise the caret belongs to the user.
|
|
253
|
+
const val = this.input.value;
|
|
254
|
+
const start = this.input.selectionStart;
|
|
255
|
+
const end = this.input.selectionEnd;
|
|
256
|
+
const noSelection = start === end;
|
|
257
|
+
const atStart = noSelection && start === 0;
|
|
258
|
+
const atEnd = noSelection && start === val.length;
|
|
259
|
+
if (atStart || (atEnd && /[\\/]$/.test(val))) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
this._goUp();
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 'End':
|
|
266
|
+
// Shift+End / Cmd+Shift+End etc are text-selection — leave alone.
|
|
267
|
+
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
|
|
268
|
+
if (this._ghost) {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
this._acceptGhost();
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_goUp() {
|
|
277
|
+
const { dir } = this._parse(this.input.value);
|
|
278
|
+
const parent = parentOf(dir);
|
|
279
|
+
if (!parent) return;
|
|
280
|
+
const display = this._displayFor(parent, true);
|
|
281
|
+
this.input.value = display;
|
|
282
|
+
this.input.selectionStart = this.input.selectionEnd = display.length;
|
|
283
|
+
this._ghost = '';
|
|
284
|
+
this._refresh();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_move(delta) {
|
|
288
|
+
if (!this._items.length) return;
|
|
289
|
+
const n = this._items.length;
|
|
290
|
+
this._selectedIndex = (this._selectedIndex + delta + n) % n;
|
|
291
|
+
// Ghost follows the selected item when it's a real entry, but stays
|
|
292
|
+
// pinned to the first real item when the user lands on a synthetic
|
|
293
|
+
// row (`..` or "Open this folder") — so Tab still autocompletes
|
|
294
|
+
// their typing instead of going silent.
|
|
295
|
+
this._setGhostFromItem(this._ghostItemFor());
|
|
296
|
+
this._renderGhost();
|
|
297
|
+
this._renderList();
|
|
298
|
+
this.list.querySelector('.qs-item.selected')
|
|
299
|
+
?.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_ghostItemFor() {
|
|
303
|
+
const synthetic = it => it.is_parent || it.is_open_here || it.is_create_here;
|
|
304
|
+
const selected = this._items[this._selectedIndex];
|
|
305
|
+
if (selected && !synthetic(selected)) return selected;
|
|
306
|
+
return this._items.find(it => !synthetic(it));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_setGhostFromItem(item) {
|
|
310
|
+
// Fish-style: no input → no ghost (placeholder owns that space).
|
|
311
|
+
// Never suggest the synthetic navigation rows (`..`, "Open this folder").
|
|
312
|
+
const val = this.input.value;
|
|
313
|
+
if (!item || !val || item.is_parent || item.is_open_here || item.is_create_here) { this._ghost = ''; return; }
|
|
314
|
+
const filter = this._currentFilter();
|
|
315
|
+
const target = item.is_dir ? item.name + '/' : item.name;
|
|
316
|
+
if (target.toLowerCase().startsWith(filter.toLowerCase())) {
|
|
317
|
+
// Preserve target's case for the remainder.
|
|
318
|
+
let rest = target.slice(filter.length);
|
|
319
|
+
// Bare path tokens (`~`, `.`, `..`) are missing the separator
|
|
320
|
+
// slash before the next segment — without this fix, Tab on `~`
|
|
321
|
+
// would yield `~dev/` instead of `~/dev/`.
|
|
322
|
+
if (val === '~' || val === '.' || val === '..') {
|
|
323
|
+
rest = '/' + rest;
|
|
324
|
+
}
|
|
325
|
+
this._ghost = rest;
|
|
326
|
+
} else {
|
|
327
|
+
this._ghost = '';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_currentFilter() {
|
|
332
|
+
const { filter } = this._parse(this.input.value);
|
|
333
|
+
return filter;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_optsFromEvent(e) {
|
|
337
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
338
|
+
if (mod && e.shiftKey) return { newTab: true };
|
|
339
|
+
if (mod) return { background: true };
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Parse the input into a mode + (dir, filter) pair.
|
|
345
|
+
*
|
|
346
|
+
* Modes:
|
|
347
|
+
* - 'empty': "" → show cwd contents
|
|
348
|
+
* - 'basename': "app" → prefix-match basenames across the project tree
|
|
349
|
+
* - 'path': "src/", "~/dev/", "/etc/p", "../tests" → strict dir mode
|
|
350
|
+
*/
|
|
351
|
+
_parse(input) {
|
|
352
|
+
const cwd = this._getCwd();
|
|
353
|
+
if (!input) return { mode: 'empty', dir: cwd, filter: '' };
|
|
354
|
+
|
|
355
|
+
const startsWithTilde = input === '~' || /^~[\\/]/.test(input);
|
|
356
|
+
const isAbsolute = isAbsolutePath(input, cwd);
|
|
357
|
+
const isRelative =
|
|
358
|
+
input === '.' || input === '..' ||
|
|
359
|
+
/^\.\.?[\\/]/.test(input);
|
|
360
|
+
// Either separator: a Windows user types (and pastes) backslashes.
|
|
361
|
+
const hasSlash = /[\\/]/.test(input);
|
|
362
|
+
|
|
363
|
+
const pathMode = startsWithTilde || isAbsolute || isRelative || hasSlash;
|
|
364
|
+
if (!pathMode) return { mode: 'basename', dir: cwd, filter: input };
|
|
365
|
+
|
|
366
|
+
// Split into (head) + (filter being typed after the last separator).
|
|
367
|
+
const lastSlash = Math.max(input.lastIndexOf('/'), input.lastIndexOf('\\'));
|
|
368
|
+
let head, filter;
|
|
369
|
+
if (lastSlash === -1) {
|
|
370
|
+
// Input is `~`, `.`, or `..` — treat as if it had a trailing slash.
|
|
371
|
+
head = input + '/';
|
|
372
|
+
filter = '';
|
|
373
|
+
} else {
|
|
374
|
+
head = input.slice(0, lastSlash + 1);
|
|
375
|
+
filter = input.slice(lastSlash + 1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let dir;
|
|
379
|
+
if (/^~[\\/]/.test(head)) {
|
|
380
|
+
dir = joinPath(CONFIG.HOME || '', head.slice(2));
|
|
381
|
+
} else if (isAbsolutePath(head, cwd)) {
|
|
382
|
+
dir = head;
|
|
383
|
+
} else {
|
|
384
|
+
// ./foo/ ../foo/ foo/bar/ (relative to cwd)
|
|
385
|
+
dir = resolvePath(cwd, head);
|
|
386
|
+
}
|
|
387
|
+
dir = stripTrailingSep(dir) || pathRoot(dir) || '/';
|
|
388
|
+
return { mode: 'path', dir, filter };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
_getCwd() {
|
|
392
|
+
// Prefer the active session's cwd; on the welcome screen there's none
|
|
393
|
+
// yet, so fall back to the server's --workspace dir (mirrors the file
|
|
394
|
+
// explorer anchor; CONFIG.WORKSPACE itself falls back to the OS home
|
|
395
|
+
// when --workspace isn't set).
|
|
396
|
+
const sessionCwd = window.app?.activeSession?.cwd;
|
|
397
|
+
if (sessionCwd) return sessionCwd;
|
|
398
|
+
return window.app?.lastCwd || CONFIG.WORKSPACE;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async _refresh() {
|
|
402
|
+
const value = this.input.value;
|
|
403
|
+
const { mode, dir, filter } = this._parse(value);
|
|
404
|
+
this._renderCurrentPath(mode, dir);
|
|
405
|
+
this._missingDir = null;
|
|
406
|
+
|
|
407
|
+
const token = ++this._reqToken;
|
|
408
|
+
let items = [];
|
|
409
|
+
try {
|
|
410
|
+
if (mode === 'empty') {
|
|
411
|
+
items = await this._listDir(dir, '');
|
|
412
|
+
} else if (mode === 'basename') {
|
|
413
|
+
items = await this._projectBasenameMatches(dir, filter);
|
|
414
|
+
} else {
|
|
415
|
+
items = await this._listDir(dir, filter);
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error('[OpenDialog]', err);
|
|
419
|
+
}
|
|
420
|
+
if (token !== this._reqToken) return;
|
|
421
|
+
|
|
422
|
+
// Typed a path that doesn't exist → offer to create it. Covers a
|
|
423
|
+
// fully-typed missing dir (`~/dev/newproj/`, listing 404s), a new
|
|
424
|
+
// name typed inside an existing dir (`~/dev/newproj`, prefix filter
|
|
425
|
+
// matches nothing), and a bare name with no slash at all
|
|
426
|
+
// (`newproj`, basename search over the project comes up empty —
|
|
427
|
+
// created relative to cwd). mkdir -p on the server also handles
|
|
428
|
+
// missing intermediate dirs.
|
|
429
|
+
const offerCreate = items.length === 0 && (
|
|
430
|
+
(mode === 'path' && (filter || this._missingDir === dir)) ||
|
|
431
|
+
(mode === 'basename' && filter)
|
|
432
|
+
);
|
|
433
|
+
if (offerCreate) {
|
|
434
|
+
const target = filter ? joinPath(dir, filter) : dir;
|
|
435
|
+
const pretty = this._formatPathForDisplay(target).replace(/[\\/]$/, '') || '/';
|
|
436
|
+
items = [{
|
|
437
|
+
type: 'action',
|
|
438
|
+
is_dir: true,
|
|
439
|
+
is_create_here: true,
|
|
440
|
+
name: '__create_here__',
|
|
441
|
+
label: S.open_dialog.create_here.label,
|
|
442
|
+
description: S.open_dialog.create_here.desc.replace('{dir}', pretty),
|
|
443
|
+
absPath: target,
|
|
444
|
+
}];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
this._items = items;
|
|
448
|
+
// Default-select the "Open this folder" action when it's present
|
|
449
|
+
// (so plain Enter opens the listed dir as a session). With `..`
|
|
450
|
+
// also added it sits at index 1; without `..` it's at index 0.
|
|
451
|
+
// Falls back to 0 when neither synthetic row exists (filter mode).
|
|
452
|
+
const ohIdx = items.findIndex(it => it.is_open_here);
|
|
453
|
+
this._selectedIndex = ohIdx >= 0 ? ohIdx : 0;
|
|
454
|
+
this._setGhostFromItem(this._ghostItemFor());
|
|
455
|
+
this._renderGhost();
|
|
456
|
+
this._renderList();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async _listDir(dir, filter) {
|
|
460
|
+
if (!dir) return [];
|
|
461
|
+
|
|
462
|
+
const cached = this._pathCache.get(dir);
|
|
463
|
+
let entries;
|
|
464
|
+
if (cached && Date.now() - cached.t < PATH_CACHE_TTL) {
|
|
465
|
+
entries = cached.entries;
|
|
466
|
+
} else {
|
|
467
|
+
const r = await fetch(`${CONFIG.API_BASE}/api/files?path=${encodeURIComponent(dir)}`);
|
|
468
|
+
if (!r.ok) {
|
|
469
|
+
// 404 → the dir doesn't exist yet; _refresh offers to create it.
|
|
470
|
+
// Other errors (403/timeout) just yield an empty listing.
|
|
471
|
+
if (r.status === 404) this._missingDir = dir;
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
const data = await r.json();
|
|
475
|
+
entries = data.files || [];
|
|
476
|
+
this._pathCache.set(dir, { t: Date.now(), entries });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const fLower = filter.toLowerCase();
|
|
480
|
+
const filtered = filter
|
|
481
|
+
? entries.filter(e => e.name.toLowerCase().startsWith(fLower))
|
|
482
|
+
// Hide dotfiles by default; user can type `.` to opt in.
|
|
483
|
+
: entries.filter(e => !e.name.startsWith('.'));
|
|
484
|
+
|
|
485
|
+
filtered.sort((a, b) => {
|
|
486
|
+
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
487
|
+
return a.name.localeCompare(b.name);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const mapped = filtered.slice(0, MAX_LIST).map(e => ({
|
|
491
|
+
type: e.is_dir ? 'dir' : 'file',
|
|
492
|
+
is_dir: e.is_dir,
|
|
493
|
+
name: e.name,
|
|
494
|
+
label: e.is_dir ? e.name + '/' : e.name,
|
|
495
|
+
description: '',
|
|
496
|
+
absPath: e.path,
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
// Synthetic rows: `..` (parent) at the very top so it matches the
|
|
500
|
+
// typical file-manager mental model, then "Open this folder" which
|
|
501
|
+
// becomes the default-selected action (Enter opens the listed dir
|
|
502
|
+
// as a new session). Only added when no filter is active — prefix-
|
|
503
|
+
// match would hide them anyway and they'd be confusing alongside
|
|
504
|
+
// filtered file results.
|
|
505
|
+
if (!filter) {
|
|
506
|
+
// Unshift order is reverse of final order — push the action
|
|
507
|
+
// first so it ends up at index 1 (or 0 when there's no parent).
|
|
508
|
+
mapped.unshift({
|
|
509
|
+
type: 'action',
|
|
510
|
+
is_dir: true,
|
|
511
|
+
is_open_here: true,
|
|
512
|
+
name: '__open_here__',
|
|
513
|
+
label: S.open_dialog.open_here.label,
|
|
514
|
+
description: S.open_dialog.open_here.desc,
|
|
515
|
+
absPath: dir,
|
|
516
|
+
});
|
|
517
|
+
const parent = parentOf(dir);
|
|
518
|
+
if (parent) {
|
|
519
|
+
mapped.unshift({
|
|
520
|
+
type: 'dir',
|
|
521
|
+
is_dir: true,
|
|
522
|
+
is_parent: true,
|
|
523
|
+
name: '..',
|
|
524
|
+
label: '../',
|
|
525
|
+
description: '',
|
|
526
|
+
absPath: parent,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return mapped;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async _projectBasenameMatches(cwd, filter) {
|
|
534
|
+
if (!cwd || !filter) return [];
|
|
535
|
+
|
|
536
|
+
const cached = this._projectCache.get(cwd);
|
|
537
|
+
let payload;
|
|
538
|
+
if (cached && Date.now() - cached.t < PROJECT_CACHE_TTL) {
|
|
539
|
+
payload = cached.payload;
|
|
540
|
+
} else {
|
|
541
|
+
// include_ignored=true: the user is picking a file to open, so
|
|
542
|
+
// .gitignore'd files (CLAUDE.md, .env, local notes) must be reachable
|
|
543
|
+
// — otherwise typing the exact basename returns "No matches" even
|
|
544
|
+
// though the file is right there on disk.
|
|
545
|
+
const r = await fetch(`${CONFIG.API_BASE}/api/files/list?cwd=${encodeURIComponent(cwd)}&include_ignored=true`);
|
|
546
|
+
if (!r.ok) return [];
|
|
547
|
+
payload = await r.json();
|
|
548
|
+
this._projectCache.set(cwd, { t: Date.now(), payload });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const recent = await this._loadRecent(cwd);
|
|
552
|
+
const fLower = filter.toLowerCase();
|
|
553
|
+
const files = payload.files || [];
|
|
554
|
+
const dirs = payload.directories || [];
|
|
555
|
+
|
|
556
|
+
const matches = [];
|
|
557
|
+
for (const d of dirs) {
|
|
558
|
+
const name = d.replace(/\/$/, '');
|
|
559
|
+
if (!name.toLowerCase().startsWith(fLower)) continue;
|
|
560
|
+
const abs = resolvePath(cwd, name);
|
|
561
|
+
matches.push({
|
|
562
|
+
type: 'dir',
|
|
563
|
+
is_dir: true,
|
|
564
|
+
name,
|
|
565
|
+
label: name + '/',
|
|
566
|
+
description: cwd,
|
|
567
|
+
absPath: abs,
|
|
568
|
+
recentRank: recent.get(abs) ?? Infinity,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
for (const path of files) {
|
|
572
|
+
const fileName = basename(path);
|
|
573
|
+
if (!fileName.toLowerCase().startsWith(fLower)) continue;
|
|
574
|
+
const abs = resolvePath(cwd, path);
|
|
575
|
+
matches.push({
|
|
576
|
+
type: 'file',
|
|
577
|
+
is_dir: false,
|
|
578
|
+
name: fileName,
|
|
579
|
+
label: fileName,
|
|
580
|
+
description: dirname(path),
|
|
581
|
+
absPath: abs,
|
|
582
|
+
recentRank: recent.get(abs) ?? Infinity,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Recent first, then dirs over files, then shallower paths, then alpha.
|
|
587
|
+
matches.sort((a, b) => {
|
|
588
|
+
if (a.recentRank !== b.recentRank) return a.recentRank - b.recentRank;
|
|
589
|
+
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
590
|
+
const ad = splitPath(a.description, a.absPath).length;
|
|
591
|
+
const bd = splitPath(b.description, b.absPath).length;
|
|
592
|
+
if (ad !== bd) return ad - bd;
|
|
593
|
+
return a.name.localeCompare(b.name);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
return matches.slice(0, MAX_BASENAME_LIST);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async _loadRecent(cwd) {
|
|
600
|
+
const cached = this._recentCache.get(cwd);
|
|
601
|
+
if (cached && Date.now() - cached.t < RECENT_TTL) return cached.map;
|
|
602
|
+
const map = new Map();
|
|
603
|
+
try {
|
|
604
|
+
const r = await fetch(
|
|
605
|
+
`${CONFIG.API_BASE}/api/shadow-db/recent-files?cwd=${encodeURIComponent(cwd)}&limit=100`
|
|
606
|
+
);
|
|
607
|
+
if (r.ok) {
|
|
608
|
+
const data = await r.json();
|
|
609
|
+
(data.files || []).forEach((f, i) => {
|
|
610
|
+
const abs = resolvePath(cwd, f.path);
|
|
611
|
+
map.set(abs, i);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
// best-effort; ghost still works without recency data
|
|
616
|
+
}
|
|
617
|
+
this._recentCache.set(cwd, { t: Date.now(), map });
|
|
618
|
+
return map;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_renderGhost() {
|
|
622
|
+
this.ghostTyped.textContent = this.input.value;
|
|
623
|
+
this.ghostRest.textContent = this._ghost || '';
|
|
624
|
+
this._updateFooterForSelection();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_renderCurrentPath(mode, dir) {
|
|
628
|
+
if (!this.pathEl) return;
|
|
629
|
+
const pretty = this._formatPathForDisplay(dir);
|
|
630
|
+
if (mode === 'basename') {
|
|
631
|
+
this.pathEl.innerHTML =
|
|
632
|
+
`<span class="od-path-label">${escapeHtml(S.open_dialog.section.project)}</span>` +
|
|
633
|
+
`<span class="od-path-text">${escapeHtml(pretty)}</span>`;
|
|
634
|
+
} else {
|
|
635
|
+
this.pathEl.innerHTML = `<span class="od-path-text">${escapeHtml(pretty)}</span>`;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
_formatPathForDisplay(dir) {
|
|
640
|
+
if (!dir) return '';
|
|
641
|
+
const home = CONFIG.HOME || '';
|
|
642
|
+
let out = dir;
|
|
643
|
+
if (home && isUnder(dir, home)) {
|
|
644
|
+
out = '~' + dir.slice(home.length);
|
|
645
|
+
}
|
|
646
|
+
return /[\\/]$/.test(out) ? out : out + pathSep(dir);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_renderList() {
|
|
650
|
+
if (!this._items.length) {
|
|
651
|
+
const txt = this.input.value.trim() ? S.open_dialog.empty.no_results : ' ';
|
|
652
|
+
this.list.innerHTML = `<div class="qs-empty">${escapeHtml(txt)}</div>`;
|
|
653
|
+
this._updateFooterForSelection();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const parts = [];
|
|
657
|
+
for (let i = 0; i < this._items.length; i++) {
|
|
658
|
+
parts.push(this._renderItem(this._items[i], i, i === this._selectedIndex));
|
|
659
|
+
}
|
|
660
|
+
this.list.innerHTML = parts.join('');
|
|
661
|
+
this.input.setAttribute('aria-activedescendant', `od-item-${this._selectedIndex}`);
|
|
662
|
+
this._updateFooterForSelection();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
_renderItem(item, index, isSelected) {
|
|
666
|
+
const filter = this._currentFilter();
|
|
667
|
+
const label = item.label;
|
|
668
|
+
let labelHtml = escapeHtml(label);
|
|
669
|
+
if (filter && label.toLowerCase().startsWith(filter.toLowerCase())) {
|
|
670
|
+
labelHtml =
|
|
671
|
+
`<mark>${escapeHtml(label.slice(0, filter.length))}</mark>` +
|
|
672
|
+
escapeHtml(label.slice(filter.length));
|
|
673
|
+
}
|
|
674
|
+
const icon = (item.is_open_here || item.is_create_here)
|
|
675
|
+
? ICONS.plus
|
|
676
|
+
: (item.is_dir ? ICONS.folder : ICONS.file);
|
|
677
|
+
const desc = item.description
|
|
678
|
+
? `<span class="qs-item-desc">${escapeHtml(item.description)}</span>`
|
|
679
|
+
: '';
|
|
680
|
+
const extraCls = (item.is_open_here || item.is_create_here) ? ' qs-item-open-here'
|
|
681
|
+
: (item.is_parent ? ' qs-item-parent' : '');
|
|
682
|
+
const cls = `qs-item qs-item-${item.type}${extraCls}${isSelected ? ' selected' : ''}`;
|
|
683
|
+
return `
|
|
684
|
+
<div class="${cls}" role="option" data-index="${index}" id="od-item-${index}" aria-selected="${isSelected}">
|
|
685
|
+
<span class="qs-item-icon">${icon}</span>
|
|
686
|
+
<span class="qs-item-text">
|
|
687
|
+
<span class="qs-item-label">${labelHtml}</span>
|
|
688
|
+
${desc}
|
|
689
|
+
</span>
|
|
690
|
+
</div>
|
|
691
|
+
`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_updateFooterForSelection() {
|
|
695
|
+
if (!this.footer) return;
|
|
696
|
+
const item = this._items[this._selectedIndex];
|
|
697
|
+
const isDir = !!item?.is_dir;
|
|
698
|
+
// "Open this folder" commits a session on Enter; "Create this folder"
|
|
699
|
+
// mkdirs + drills in; every other dir (regular folders + `..`) drills
|
|
700
|
+
// in. Footer hint follows suit.
|
|
701
|
+
const isOpenAction = !!item?.is_open_here;
|
|
702
|
+
const isCreateAction = !!item?.is_create_here;
|
|
703
|
+
this.footer.classList.toggle('od-state-file', !!(item && !isDir));
|
|
704
|
+
this.footer.classList.toggle('od-state-dir-nav', isDir && !isOpenAction && !isCreateAction);
|
|
705
|
+
this.footer.classList.toggle('od-state-dir-create', isCreateAction);
|
|
706
|
+
this.footer.classList.toggle('od-state-dir-open', isDir && isOpenAction);
|
|
707
|
+
this.footer.classList.toggle('od-state-ghost', !!this._ghost);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
_acceptGhost() {
|
|
711
|
+
if (!this._ghost) return;
|
|
712
|
+
const newValue = this.input.value + this._ghost;
|
|
713
|
+
this.input.value = newValue;
|
|
714
|
+
this.input.selectionStart = this.input.selectionEnd = newValue.length;
|
|
715
|
+
this._ghost = '';
|
|
716
|
+
this._renderGhost();
|
|
717
|
+
// After accepting, if the new value ends with '/', _refresh will fetch
|
|
718
|
+
// the new directory's contents and pick a fresh ghost.
|
|
719
|
+
this._refresh();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async _submit(opts = {}) {
|
|
723
|
+
const item = this._items[this._selectedIndex];
|
|
724
|
+
if (!item) return;
|
|
725
|
+
// The synthetic `..` is a navigation row, not a destination. Walk up
|
|
726
|
+
// and keep the dialog open instead of opening a session.
|
|
727
|
+
if (item.is_parent) { this._goUp(); return; }
|
|
728
|
+
// "Create this folder" → mkdir then drill into it, keeping the dialog
|
|
729
|
+
// open. The freshly-listed dir offers "Open this folder" as the
|
|
730
|
+
// default-selected row, so a session is still just one Enter away.
|
|
731
|
+
if (item.is_create_here) {
|
|
732
|
+
await this._createAndEnter(item.absPath);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
// A regular folder → drill into it (browse), keeping the dialog open.
|
|
736
|
+
// Opening as a session is reserved for the explicit "Open this folder"
|
|
737
|
+
// row, which is auto-selected after every drill-in (one Enter away).
|
|
738
|
+
if (item.is_dir && !item.is_open_here) {
|
|
739
|
+
this._drillInto(item);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.hide();
|
|
743
|
+
try {
|
|
744
|
+
if (item.is_dir) this._openSessionAt(item.absPath);
|
|
745
|
+
else this._openFile(item.absPath, opts);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error('[OpenDialog] submit:', err);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async _createAndEnter(dir) {
|
|
752
|
+
try {
|
|
753
|
+
const r = await fetch(
|
|
754
|
+
`${CONFIG.API_BASE}/api/mkdir`,
|
|
755
|
+
{
|
|
756
|
+
method: 'POST',
|
|
757
|
+
headers: { 'Content-Type': 'application/json' },
|
|
758
|
+
body: JSON.stringify({ path: dir }),
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
const data = await r.json().catch(() => ({}));
|
|
762
|
+
if (!r.ok) {
|
|
763
|
+
// `detail` is a *list* of dicts on a 422 (Pydantic) — stringifying
|
|
764
|
+
// it raw is what turned this toast into "[object Object]".
|
|
765
|
+
showToast(S.open_dialog.create_failed.replace(
|
|
766
|
+
'{error}', extractApiError(data, `HTTP ${r.status}`)));
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Bust the cached listings (the dir itself + its parent, whose
|
|
770
|
+
// cached entry list is now missing the new child).
|
|
771
|
+
this._pathCache.delete(dir);
|
|
772
|
+
const parent = parentOf(dir);
|
|
773
|
+
if (parent) this._pathCache.delete(parent);
|
|
774
|
+
this._drillInto({ is_dir: true, absPath: data.path || dir });
|
|
775
|
+
this.input.focus();
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error('[OpenDialog] create:', err);
|
|
778
|
+
showToast(S.open_dialog.create_failed.replace('{error}', err.message));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
_openFile(absPath, opts = {}) {
|
|
783
|
+
const tabCtrl = window.app?.tabCtrl;
|
|
784
|
+
if (opts.background && tabCtrl?.openFilePreviewTab) {
|
|
785
|
+
tabCtrl.openFilePreviewTab(absPath, null, { background: true });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (opts.newTab && tabCtrl?.openFilePreviewTab) {
|
|
789
|
+
tabCtrl.openFilePreviewTab(absPath, null, { newTab: true });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
window.app?.previewFile?.(absPath);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
_openSessionAt(cwd) {
|
|
796
|
+
const app = window.app;
|
|
797
|
+
if (!app?.sessionManager) return;
|
|
798
|
+
const name = basename(cwd) || 'New Session';
|
|
799
|
+
const session = app.sessionManager.create({ name });
|
|
800
|
+
if (!session) return;
|
|
801
|
+
session.cwd = cwd;
|
|
802
|
+
if (app.tabCtrl?.switchToSession) app.tabCtrl.switchToSession(session);
|
|
803
|
+
else app.switchSession?.(session);
|
|
804
|
+
app.renderTabs?.();
|
|
805
|
+
session.connect();
|
|
806
|
+
app.sessionManager.saveSessions?.();
|
|
807
|
+
app.addToHistory?.(cwd);
|
|
808
|
+
app.fileExplorer?.setHomePath?.(cwd);
|
|
809
|
+
app.fetchProjectCommands?.(cwd);
|
|
810
|
+
app.els?.connectionBar?.classList.remove('visible');
|
|
811
|
+
// If the user typed a message on the welcome screen before picking
|
|
812
|
+
// a project, send it now on the new session.
|
|
813
|
+
if (app._pendingWelcomeMessage) {
|
|
814
|
+
const msg = app._pendingWelcomeMessage;
|
|
815
|
+
app._pendingWelcomeMessage = null;
|
|
816
|
+
if (app.els?.messageInput) app.els.messageInput.value = '';
|
|
817
|
+
app.syncInputHighlightBackdrop?.();
|
|
818
|
+
app._sendMessageDirect?.(msg);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_showContextMenu(x, y) {
|
|
823
|
+
const item = this._items[this._selectedIndex];
|
|
824
|
+
if (!item) return;
|
|
825
|
+
const menu = window.app?.contextMenu || (this._ctxMenu ||= new ContextMenu());
|
|
826
|
+
const tabCtrl = window.app?.tabCtrl;
|
|
827
|
+
const close = (fn) => { this.hide(); fn?.(); };
|
|
828
|
+
const items = item.is_dir
|
|
829
|
+
? [
|
|
830
|
+
{ label: S.open_dialog.menu.open_session, action: () => close(() => this._openSessionAt(item.absPath)) },
|
|
831
|
+
{ label: S.open_dialog.menu.drill_in, action: () => { this._drillInto(item); this.input.focus(); } },
|
|
832
|
+
{ type: 'separator' },
|
|
833
|
+
{ label: S.context_menus.file.copy_path, action: async () => { if (await copyToClipboard(item.absPath)) showToast(S.toast.copied); } },
|
|
834
|
+
]
|
|
835
|
+
: [
|
|
836
|
+
{ label: S.context_menus.file.preview, action: () => close(() => window.app?.previewFile?.(item.absPath)) },
|
|
837
|
+
{ label: S.context_menus.file.open_in_new_tab, action: () => close(() => tabCtrl?.openFilePreviewTab?.(item.absPath, null, { newTab: true })) },
|
|
838
|
+
{ label: S.context_menus.file.open_in_background, action: () => close(() => tabCtrl?.openFilePreviewTab?.(item.absPath, null, { background: true })) },
|
|
839
|
+
{ type: 'separator' },
|
|
840
|
+
{ label: S.context_menus.file.copy_full_path, action: async () => { if (await copyToClipboard(item.absPath)) showToast(S.toast.copied); } },
|
|
841
|
+
];
|
|
842
|
+
menu.show(x, y, items);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
_drillInto(item) {
|
|
846
|
+
if (!item?.is_dir) return;
|
|
847
|
+
const display = this._displayFor(item.absPath, true);
|
|
848
|
+
this.input.value = display;
|
|
849
|
+
this.input.selectionStart = this.input.selectionEnd = display.length;
|
|
850
|
+
this._ghost = '';
|
|
851
|
+
this._refresh();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
_displayFor(absPath, isDir) {
|
|
855
|
+
const cwd = this._getCwd();
|
|
856
|
+
const home = CONFIG.HOME || '';
|
|
857
|
+
const original = this.input.value;
|
|
858
|
+
let out;
|
|
859
|
+
if (home && absPath === home) {
|
|
860
|
+
out = '~';
|
|
861
|
+
} else if (/^~[\\/]/.test(original) && home && isUnder(absPath, home) && absPath !== home) {
|
|
862
|
+
out = '~' + pathSep(absPath) + relDisplay(absPath, home);
|
|
863
|
+
} else if (isAbsolutePath(original, cwd) || !cwd) {
|
|
864
|
+
out = absPath;
|
|
865
|
+
} else if (absPath === cwd) {
|
|
866
|
+
out = './';
|
|
867
|
+
} else if (isUnder(absPath, cwd) && absPath !== cwd) {
|
|
868
|
+
out = relDisplay(absPath, cwd);
|
|
869
|
+
} else if (home && isUnder(absPath, home) && absPath !== home) {
|
|
870
|
+
out = '~' + pathSep(absPath) + relDisplay(absPath, home);
|
|
871
|
+
} else {
|
|
872
|
+
out = absPath;
|
|
873
|
+
}
|
|
874
|
+
if (isDir && !/[\\/]$/.test(out)) out += pathSep(absPath);
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export const OpenDialog = new OpenDialogClass();
|