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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview file-change polling
|
|
3
|
+
*
|
|
4
|
+
* Polls file mtime via /api/file/stat while the preview is visible.
|
|
5
|
+
* - View mode / edit without changes: silent auto-reload
|
|
6
|
+
* - Edit mode with unsaved changes: shows notification bar
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { state, fns } from './preview-state.js';
|
|
10
|
+
import { statFile, fetchFile, showToast } from './preview-utils.js';
|
|
11
|
+
import { isInlineEditInProgress } from './preview-inline-edit.js';
|
|
12
|
+
import S from '../strings.js';
|
|
13
|
+
|
|
14
|
+
const POLL_INTERVAL_MS = 5000;
|
|
15
|
+
let pollTimer = null;
|
|
16
|
+
|
|
17
|
+
export function startPolling() {
|
|
18
|
+
if (pollTimer) return;
|
|
19
|
+
pollTimer = setInterval(checkForChanges, POLL_INTERVAL_MS);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function stopPolling() {
|
|
23
|
+
if (pollTimer) {
|
|
24
|
+
clearInterval(pollTimer);
|
|
25
|
+
pollTimer = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function checkForChanges() {
|
|
30
|
+
// Skip: no file, scratch mode, loading, or no known mtime
|
|
31
|
+
if (!state.currentPath || state.isScratch || state.isLoading || !state.mtime) return;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const stat = await statFile(state.currentPath);
|
|
35
|
+
if (!stat.exists) return;
|
|
36
|
+
if (stat.mtime <= state.mtime) return;
|
|
37
|
+
|
|
38
|
+
// File changed on disk
|
|
39
|
+
if (state.viewMode === 'edit' && state.modified) {
|
|
40
|
+
showChangedNotification();
|
|
41
|
+
} else if (isInlineEditInProgress()) {
|
|
42
|
+
// An open inline-edit textarea holds unsaved keystrokes; a
|
|
43
|
+
// silentReload() → rerenderContent() would wipe it. Defer — the
|
|
44
|
+
// mtime stays ahead, so we'll reload on a later tick once it closes.
|
|
45
|
+
return;
|
|
46
|
+
} else {
|
|
47
|
+
await silentReload();
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Network error etc — skip this cycle
|
|
51
|
+
console.warn('[preview-poll] stat failed:', err.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function silentReload() {
|
|
56
|
+
try {
|
|
57
|
+
const { content, mtime } = await fetchFile(state.currentPath);
|
|
58
|
+
state.content = content;
|
|
59
|
+
state.mtime = mtime;
|
|
60
|
+
// Update editBuffer if in edit mode (no unsaved changes)
|
|
61
|
+
if (state.viewMode === 'edit') {
|
|
62
|
+
state.editBuffer = content;
|
|
63
|
+
if (state.editor) {
|
|
64
|
+
state.editor.setContent(content);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
fns.rerenderContent();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn('[preview-poll] reload failed:', err.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function showChangedNotification() {
|
|
74
|
+
const container = fns.findPreviewContainer();
|
|
75
|
+
if (!container) return;
|
|
76
|
+
// Don't show duplicate
|
|
77
|
+
if (container.querySelector('.preview-changed-bar')) return;
|
|
78
|
+
|
|
79
|
+
const bar = document.createElement('div');
|
|
80
|
+
bar.className = 'preview-changed-bar';
|
|
81
|
+
bar.innerHTML = `
|
|
82
|
+
<span class="preview-changed-text">File changed on disk</span>
|
|
83
|
+
<button class="preview-changed-btn reload">Reload</button>
|
|
84
|
+
<button class="preview-changed-btn dismiss">Dismiss</button>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
bar.querySelector('.reload').addEventListener('click', async () => {
|
|
88
|
+
bar.remove();
|
|
89
|
+
await silentReload();
|
|
90
|
+
showToast(S.toast.reloaded);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
bar.querySelector('.dismiss').addEventListener('click', () => {
|
|
94
|
+
bar.remove();
|
|
95
|
+
// Update mtime so we don't re-trigger until next change
|
|
96
|
+
statFile(state.currentPath).then(s => {
|
|
97
|
+
if (s.exists) state.mtime = s.mtime;
|
|
98
|
+
}).catch(() => {});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Insert at top of the preview body
|
|
102
|
+
const body = container.querySelector('.preview-body');
|
|
103
|
+
if (body) {
|
|
104
|
+
body.insertBefore(bar, body.firstChild);
|
|
105
|
+
} else {
|
|
106
|
+
container.insertBefore(bar, container.firstChild);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview rendering
|
|
3
|
+
*
|
|
4
|
+
* Handles all HTML rendering for the preview widget:
|
|
5
|
+
* renderPreview, renderBody, renderBodyWithState, highlightContent, scrollToLine
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { state, pluginHelpers, isEditMode, isHistoryMode, isEditable, wrapLines } from './preview-state.js';
|
|
9
|
+
import { getHighlightLanguage, getEffectiveLanguage, addLineNumbers, AVAILABLE_LANGUAGES } from './preview-utils.js';
|
|
10
|
+
import { escapeHtml } from '../utils.js';
|
|
11
|
+
import { detectLanguage } from '../file-tabs.js';
|
|
12
|
+
import { downloadBtnHtml } from '../preview-plugins/plugin-helpers.js';
|
|
13
|
+
import { highlightCodeToLines } from '../editor-view.js';
|
|
14
|
+
import { renderHistoryBody } from './preview-history.js';
|
|
15
|
+
import { withChords } from '../shortcuts.js';
|
|
16
|
+
import S from '../strings.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Empty state — no file is open. Shown when the preview is summoned on its own
|
|
20
|
+
* (rail button / Alt+V with nothing previewed yet) rather than by a file click,
|
|
21
|
+
* so it has to offer a way IN: the button opens the same path picker as Cmd+O.
|
|
22
|
+
*
|
|
23
|
+
* The key hint is read from the live shortcut registry rather than hardcoded,
|
|
24
|
+
* so it follows a user rebind (and the Mac/other split) the way the rail
|
|
25
|
+
* tooltips do.
|
|
26
|
+
*/
|
|
27
|
+
function emptyStateHtml() {
|
|
28
|
+
const key = window.app?.shortcutManager?.getShortcutKeys('openDialog')?.[0];
|
|
29
|
+
const kbd = key ? `<kbd>${escapeHtml(key)}</kbd>` : '';
|
|
30
|
+
|
|
31
|
+
return `
|
|
32
|
+
<div class="preview-body">
|
|
33
|
+
<div class="preview-empty">
|
|
34
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
35
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
36
|
+
<polyline points="14 2 14 8 20 8"/>
|
|
37
|
+
</svg>
|
|
38
|
+
<span>${escapeHtml(S.widgets.file_preview.empty_title)}</span>
|
|
39
|
+
<span class="preview-empty-hint">${escapeHtml(S.widgets.file_preview.empty_hint)}</span>
|
|
40
|
+
<button type="button" class="preview-empty-open" data-action="open-file">
|
|
41
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
42
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
|
43
|
+
</svg>
|
|
44
|
+
<span>${escapeHtml(S.widgets.file_preview.empty_open)}</span>
|
|
45
|
+
${kbd}
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function renderPreview() {
|
|
53
|
+
const body = renderBody();
|
|
54
|
+
|
|
55
|
+
return `
|
|
56
|
+
<div class="file-preview-widget">
|
|
57
|
+
${body}
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderBody() {
|
|
63
|
+
if (state.isLoading) {
|
|
64
|
+
return `
|
|
65
|
+
<div class="preview-body">
|
|
66
|
+
<div class="preview-loading">
|
|
67
|
+
<div class="loading-spinner"></div>
|
|
68
|
+
<span>Loading...</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (state.error) {
|
|
75
|
+
return `
|
|
76
|
+
<div class="preview-body">
|
|
77
|
+
<div class="preview-error">
|
|
78
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
79
|
+
<circle cx="12" cy="12" r="10"/>
|
|
80
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
81
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
82
|
+
</svg>
|
|
83
|
+
<span>${escapeHtml(state.error)}</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!state.currentPath && !state.isScratch) {
|
|
90
|
+
return emptyStateHtml();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Non-editable plugin rendering (image, excalidraw, chart):
|
|
94
|
+
// These return full HTML including their own toolbar, so return directly.
|
|
95
|
+
if (state.plugin && !state.plugin.editable) {
|
|
96
|
+
const pluginHtml = state.plugin.renderBody(state, pluginHelpers);
|
|
97
|
+
if (pluginHtml !== null) return pluginHtml;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// View mode toggle — shown for all editable text files (not scratch, which is always edit)
|
|
101
|
+
let viewToggle = '';
|
|
102
|
+
if (isEditable() && !state.isScratch) {
|
|
103
|
+
const tabs = [
|
|
104
|
+
{ mode: 'code', label: 'Code', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>` },
|
|
105
|
+
];
|
|
106
|
+
// Plugin-defined view modes (e.g. 'rendered' for markdown, 'table' for CSV)
|
|
107
|
+
if (state.plugin?.viewModes) {
|
|
108
|
+
tabs.push(...state.plugin.viewModes);
|
|
109
|
+
}
|
|
110
|
+
tabs.push({ mode: 'edit', label: S.preview?.tab_edit || 'Edit', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>` });
|
|
111
|
+
// History tab — shadow-git diff browser. Only when we have a backing file path.
|
|
112
|
+
if (state.currentPath) {
|
|
113
|
+
tabs.push({ mode: 'history', label: S.preview?.tab_history || 'History', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>` });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
viewToggle = `<div class="preview-view-toggle">${tabs.map(t =>
|
|
117
|
+
`<button class="toggle-btn ${state.viewMode === t.mode ? 'active' : ''} ${t.mode === 'edit' && state.modified ? 'modified' : ''}" data-mode="${t.mode}">${t.icon} ${t.label}</button>`
|
|
118
|
+
).join('')}</div>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Line range badge (shows when viewing specific lines) - clickable to scroll back
|
|
122
|
+
let lineBadge = '';
|
|
123
|
+
if (state.lineRange) {
|
|
124
|
+
const { start, end } = state.lineRange;
|
|
125
|
+
const count = end - start + 1;
|
|
126
|
+
lineBadge = `
|
|
127
|
+
<button class="preview-line-badge" data-scroll-line="${start}" data-tooltip="Scroll to highlighted lines">
|
|
128
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
129
|
+
<path d="M4 6h16M4 12h16M4 18h16"/>
|
|
130
|
+
</svg>
|
|
131
|
+
<span>Lines ${start}\u2013${end}</span>
|
|
132
|
+
<span class="line-count">(${count} line${count > 1 ? 's' : ''})</span>
|
|
133
|
+
</button>
|
|
134
|
+
`;
|
|
135
|
+
} else if (state.highlightLines?.length) {
|
|
136
|
+
const line = state.highlightLines[0];
|
|
137
|
+
lineBadge = `
|
|
138
|
+
<button class="preview-line-badge" data-scroll-line="${line}" data-tooltip="Scroll to highlighted line">
|
|
139
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
140
|
+
<path d="M4 6h16M4 12h16M4 18h16"/>
|
|
141
|
+
</svg>
|
|
142
|
+
<span>Line ${line}</span>
|
|
143
|
+
</button>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Language selector (code + edit views)
|
|
148
|
+
const effectiveLang = getEffectiveLanguage();
|
|
149
|
+
const isOverridden = state.languageOverride !== null;
|
|
150
|
+
const languageSelector = (!isHistoryMode() && (!state.plugin?.viewModes || state.viewMode === 'code' || state.viewMode === 'edit')) ? `
|
|
151
|
+
<div class="preview-language-selector ${isOverridden ? 'overridden' : ''}">
|
|
152
|
+
<select class="language-select" data-tooltip="Syntax highlighting language">
|
|
153
|
+
${AVAILABLE_LANGUAGES.map(l =>
|
|
154
|
+
`<option value="${l.value}" ${effectiveLang === l.value ? 'selected' : ''}>${l.label}</option>`
|
|
155
|
+
).join('')}
|
|
156
|
+
</select>
|
|
157
|
+
${isOverridden ? `<button class="reset-language-btn" data-tooltip="Reset to auto-detected (${state.language})">×</button>` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
` : '';
|
|
160
|
+
|
|
161
|
+
// Wrap toggle button (for code and edit views)
|
|
162
|
+
const wrapToggle = (!isHistoryMode() && (!state.plugin?.viewModes || state.viewMode === 'code' || state.viewMode === 'edit')) ? `
|
|
163
|
+
<button class="preview-wrap-toggle ${wrapLines ? 'active' : ''}" data-action="wrap" data-tooltip="${wrapLines ? 'Disable line wrap' : 'Enable line wrap'}">
|
|
164
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
165
|
+
<path d="M3 6h18M3 12h15M3 18h18"/>
|
|
166
|
+
<path d="M19 12v3a2 2 0 0 1-2 2h-2"/>
|
|
167
|
+
<polyline points="13 15 15 17 13 19"/>
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
` : '';
|
|
171
|
+
|
|
172
|
+
// Search button (shown for text-based files; hidden in history view).
|
|
173
|
+
// Stays visible while the search bar is open — lit, and re-focusing the
|
|
174
|
+
// existing query rather than reopening a blank search.
|
|
175
|
+
const searchButton = (isEditable() && !isHistoryMode()) ? `
|
|
176
|
+
<button class="preview-search-btn ${state.search.active ? 'active' : ''}" data-action="search" data-tooltip="${withChords(state.search.active ? (S.preview?.search_focus || 'Focus search ({key:Ctrl+F})') : (S.preview?.search_open || 'Search ({key:Ctrl+F})'))}">
|
|
177
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
178
|
+
<circle cx="11" cy="11" r="8"/>
|
|
179
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
180
|
+
</svg>
|
|
181
|
+
</button>
|
|
182
|
+
` : '';
|
|
183
|
+
|
|
184
|
+
// Download link button
|
|
185
|
+
const downloadButton = state.currentPath ? downloadBtnHtml() : '';
|
|
186
|
+
|
|
187
|
+
// Plugin-contributed toolbar controls (e.g. markdown's inline-edit toggle).
|
|
188
|
+
// The plugin decides per viewMode; history view never gets them.
|
|
189
|
+
const pluginControls = (!isHistoryMode() && state.plugin?.renderToolbarControls)
|
|
190
|
+
? (state.plugin.renderToolbarControls(state, pluginHelpers) || '')
|
|
191
|
+
: '';
|
|
192
|
+
|
|
193
|
+
// Edit-mode search buttons — open the CodeMirror search panel (the code
|
|
194
|
+
// view's own search bar doesn't exist in edit mode). Two entry points:
|
|
195
|
+
// plain search, and search & replace (opens with the replace row expanded).
|
|
196
|
+
const editorSearchButtons = isEditMode() ? `
|
|
197
|
+
<button class="preview-search-btn" data-action="editor-search" data-tooltip="${withChords(S.preview?.editor_search || 'Search ({key:Ctrl+F})')}">
|
|
198
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
199
|
+
<circle cx="11" cy="11" r="8"/>
|
|
200
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
201
|
+
</svg>
|
|
202
|
+
</button>
|
|
203
|
+
<button class="preview-search-btn" data-action="editor-replace" data-tooltip="${S.preview?.editor_replace || 'Search & replace'}">
|
|
204
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
205
|
+
<circle cx="10" cy="10" r="7"/>
|
|
206
|
+
<line x1="21" y1="21" x2="15" y2="15"/>
|
|
207
|
+
<path d="M7 10h4"/>
|
|
208
|
+
<polyline points="9.5 7.5 12 10 9.5 12.5"/>
|
|
209
|
+
</svg>
|
|
210
|
+
</button>
|
|
211
|
+
` : '';
|
|
212
|
+
|
|
213
|
+
// Search bar (shown when search is active)
|
|
214
|
+
const searchBar = state.search.active ? `
|
|
215
|
+
<div class="preview-search-bar">
|
|
216
|
+
<input type="text" class="preview-search-input" placeholder="Search..." value="${escapeHtml(state.search.query)}" autofocus>
|
|
217
|
+
<span class="preview-search-count">${state.search.matches.length > 0 ? `${state.search.currentIndex + 1} of ${state.search.matches.length}` : (state.search.query ? 'No results' : '')}</span>
|
|
218
|
+
<button class="preview-search-nav" data-action="search-prev" data-tooltip="Previous (Shift+Enter)" ${state.search.matches.length === 0 ? 'disabled' : ''}>
|
|
219
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
220
|
+
<polyline points="18 15 12 9 6 15"/>
|
|
221
|
+
</svg>
|
|
222
|
+
</button>
|
|
223
|
+
<button class="preview-search-nav" data-action="search-next" data-tooltip="Next (Enter)" ${state.search.matches.length === 0 ? 'disabled' : ''}>
|
|
224
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
225
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
226
|
+
</svg>
|
|
227
|
+
</button>
|
|
228
|
+
<button class="preview-search-close" data-action="search-close" data-tooltip="Close (Escape)">
|
|
229
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
230
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
231
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
232
|
+
</svg>
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
` : '';
|
|
236
|
+
|
|
237
|
+
// Right-side toolbar controls depend on viewMode
|
|
238
|
+
let rightControls = '';
|
|
239
|
+
if (isEditMode() && state.isScratch) {
|
|
240
|
+
// Scratch mode: language selector + Save As
|
|
241
|
+
rightControls = `
|
|
242
|
+
${languageSelector}
|
|
243
|
+
${editorSearchButtons}
|
|
244
|
+
${wrapToggle}
|
|
245
|
+
<span class="preview-modified-dot ${state.modified ? 'visible' : ''}" data-tooltip="Unsaved changes"></span>
|
|
246
|
+
<button class="preview-edit-btn save-as-btn" data-action="save-as" data-tooltip="Save to file">
|
|
247
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
248
|
+
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
|
249
|
+
<polyline points="17 21 17 13 7 13 7 21"/>
|
|
250
|
+
<polyline points="7 3 7 8 15 8"/>
|
|
251
|
+
</svg>
|
|
252
|
+
Save As
|
|
253
|
+
</button>
|
|
254
|
+
`;
|
|
255
|
+
} else if (isEditMode()) {
|
|
256
|
+
rightControls = `
|
|
257
|
+
${languageSelector}
|
|
258
|
+
${editorSearchButtons}
|
|
259
|
+
${wrapToggle}
|
|
260
|
+
<span class="preview-modified-dot ${state.modified ? 'visible' : ''}" data-tooltip="Unsaved changes"></span>
|
|
261
|
+
<button class="preview-edit-btn save-btn" data-action="edit-save" data-tooltip="Save (Ctrl+S)" ${state.saving ? 'disabled' : ''}>
|
|
262
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
263
|
+
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
|
264
|
+
<polyline points="17 21 17 13 7 13 7 21"/>
|
|
265
|
+
<polyline points="7 3 7 8 15 8"/>
|
|
266
|
+
</svg>
|
|
267
|
+
${state.saving ? 'Saving...' : 'Save'}
|
|
268
|
+
</button>
|
|
269
|
+
`;
|
|
270
|
+
} else {
|
|
271
|
+
rightControls = `
|
|
272
|
+
${languageSelector}
|
|
273
|
+
${pluginControls}
|
|
274
|
+
${searchButton}
|
|
275
|
+
${downloadButton}
|
|
276
|
+
${wrapToggle}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Unified toolbar: view tabs + right-side controls.
|
|
281
|
+
//
|
|
282
|
+
// An open search bar takes over the LEFT of the row (the view tabs and line
|
|
283
|
+
// badge yield the width to it), but the right-side controls always stay put
|
|
284
|
+
// — an open search is no reason to lose the edit toggle / download / wrap.
|
|
285
|
+
// The spacer is dropped while searching: it and .preview-search-bar are both
|
|
286
|
+
// flex:1, so keeping both would split the row and starve the input.
|
|
287
|
+
const hasToolbar = viewToggle || lineBadge || rightControls || state.search.active;
|
|
288
|
+
const toolbar = hasToolbar ? `
|
|
289
|
+
<div class="preview-toolbar">
|
|
290
|
+
${state.search.active ? searchBar : ''}
|
|
291
|
+
${!state.search.active ? viewToggle : ''}
|
|
292
|
+
${!state.search.active ? lineBadge : ''}
|
|
293
|
+
${!state.search.active ? '<div class="toolbar-spacer"></div>' : ''}
|
|
294
|
+
${rightControls}
|
|
295
|
+
</div>
|
|
296
|
+
` : '';
|
|
297
|
+
|
|
298
|
+
// Edit view (CodeMirror editor)
|
|
299
|
+
if (isEditMode()) {
|
|
300
|
+
return `
|
|
301
|
+
${toolbar}
|
|
302
|
+
<div class="preview-body preview-edit-body">
|
|
303
|
+
<div class="preview-cm-container">
|
|
304
|
+
<div class="cm-loading">Loading editor...</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// History view (shadow-git diff browser)
|
|
311
|
+
if (isHistoryMode()) {
|
|
312
|
+
return `
|
|
313
|
+
${toolbar}
|
|
314
|
+
${renderHistoryBody()}
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Plugin custom view mode (e.g. markdown 'rendered', CSV 'table')
|
|
319
|
+
// Plugin returns HTML for its custom mode, or null to fall through to code view
|
|
320
|
+
if (state.plugin && state.viewMode !== 'code' && state.viewMode !== 'edit') {
|
|
321
|
+
const pluginHtml = state.plugin.renderBody(state, pluginHelpers);
|
|
322
|
+
if (pluginHtml !== null) {
|
|
323
|
+
return `
|
|
324
|
+
${toolbar}
|
|
325
|
+
${pluginHtml}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Text/code content
|
|
331
|
+
const hlLang = getHighlightLanguage(getEffectiveLanguage());
|
|
332
|
+
const linesHtml = addLineNumbers(state.content || '', state.highlightLines, state.lineRange);
|
|
333
|
+
const wrapClass = wrapLines ? ' wrap-lines' : '';
|
|
334
|
+
|
|
335
|
+
return `
|
|
336
|
+
${toolbar}
|
|
337
|
+
<div class="preview-body">
|
|
338
|
+
<div class="preview-code-wrapper${wrapClass}">
|
|
339
|
+
<pre class="preview-code" data-language="${hlLang}"><code class="language-${hlLang}">${linesHtml}</code></pre>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Render body with a provided state object (for tab-specific rendering)
|
|
347
|
+
*/
|
|
348
|
+
export function renderBodyWithState(s) {
|
|
349
|
+
if (s.isLoading) {
|
|
350
|
+
return `
|
|
351
|
+
<div class="preview-body">
|
|
352
|
+
<div class="preview-loading">
|
|
353
|
+
<div class="loading-spinner"></div>
|
|
354
|
+
<span>Loading...</span>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (s.error) {
|
|
361
|
+
return `
|
|
362
|
+
<div class="preview-body">
|
|
363
|
+
<div class="preview-error">
|
|
364
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
365
|
+
<circle cx="12" cy="12" r="10"/>
|
|
366
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
367
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
368
|
+
</svg>
|
|
369
|
+
<span>${escapeHtml(s.error)}</span>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!s.currentPath) {
|
|
376
|
+
return emptyStateHtml();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Plugin rendering
|
|
380
|
+
if (s.plugin) {
|
|
381
|
+
const pluginHtml = s.plugin.renderBody(s, pluginHelpers);
|
|
382
|
+
if (pluginHtml !== null) return pluginHtml;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Text/code content
|
|
386
|
+
const hlLang = getHighlightLanguage(s.language || detectLanguage(s.currentPath));
|
|
387
|
+
const linesHtml = addLineNumbers(s.content || '', s.highlightLines, s.lineRange);
|
|
388
|
+
const wrapClass = s.wrapLines ? ' wrap-lines' : '';
|
|
389
|
+
|
|
390
|
+
return `
|
|
391
|
+
<div class="preview-body">
|
|
392
|
+
<div class="preview-code-wrapper${wrapClass}">
|
|
393
|
+
<pre class="preview-code" data-language="${hlLang}"><code class="language-${hlLang}">${linesHtml}</code></pre>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Apply syntax highlighting to code content.
|
|
401
|
+
*
|
|
402
|
+
* Primary path: the SAME lezer parser + tagHighlighter the CodeMirror editor
|
|
403
|
+
* uses (highlightCodeToLines from editor-view.js), so the Code view renders
|
|
404
|
+
* identically to Edit mode — one engine, one palette (.tok-* classes in
|
|
405
|
+
* 67-editor-view.css). Falls back to per-line hljs for languages without a
|
|
406
|
+
* CM parser (makefile) and for oversized files. Async: the plain pre-render
|
|
407
|
+
* from renderBody stays visible until the highlighted swap lands.
|
|
408
|
+
*/
|
|
409
|
+
export function highlightContent(container) {
|
|
410
|
+
if (!state.content) return;
|
|
411
|
+
if (!container.querySelector('.preview-code-wrapper')) return;
|
|
412
|
+
|
|
413
|
+
const content = state.content;
|
|
414
|
+
const language = getEffectiveLanguage();
|
|
415
|
+
|
|
416
|
+
highlightCodeToLines(content, language).then(result => {
|
|
417
|
+
// Supersede guards: content/language switched while highlighting, the
|
|
418
|
+
// code wrapper re-rendered away, or a search is actively marking lines
|
|
419
|
+
// (closeSearch triggers a fresh rerender + highlight pass anyway).
|
|
420
|
+
const codeWrapper = container.querySelector('.preview-code-wrapper');
|
|
421
|
+
if (!codeWrapper) return;
|
|
422
|
+
if (state.content !== content || getEffectiveLanguage() !== language) return;
|
|
423
|
+
if (codeWrapper.querySelector('.search-match')) return;
|
|
424
|
+
|
|
425
|
+
if (result) {
|
|
426
|
+
renderCodeLines(codeWrapper, result.lines, getHighlightLanguage(language), result.folds);
|
|
427
|
+
} else {
|
|
428
|
+
highlightContentHljs(codeWrapper, content, language);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Fallback: per-line hljs highlighting (the pre-lezer engine). Kept for
|
|
435
|
+
* languages the CM bundle has no parser for and for very large files.
|
|
436
|
+
*/
|
|
437
|
+
function highlightContentHljs(codeWrapper, content, language) {
|
|
438
|
+
if (!window.hljs) return;
|
|
439
|
+
const hlLang = getHighlightLanguage(language);
|
|
440
|
+
const htmlLines = content.split('\n').map(line => {
|
|
441
|
+
try {
|
|
442
|
+
return window.hljs.highlight(line || ' ', { language: hlLang, ignoreIllegals: true }).value;
|
|
443
|
+
} catch {
|
|
444
|
+
return escapeHtml(line) || ' ';
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
renderCodeLines(codeWrapper, htmlLines, hlLang);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Rebuild the code DOM from pre-highlighted per-line HTML, reapplying
|
|
452
|
+
* line-state classes (highlight ranges, flash) from state. `folds` (from the
|
|
453
|
+
* lezer path) renders the same collapse arrows the editor's fold gutter
|
|
454
|
+
* shows, in a fixed-width cell so Code and Edit line up pixel-for-pixel.
|
|
455
|
+
*/
|
|
456
|
+
function renderCodeLines(codeWrapper, htmlLines, langLabel, folds) {
|
|
457
|
+
const highlightSet = new Set(state.highlightLines || []);
|
|
458
|
+
const lineRange = state.lineRange;
|
|
459
|
+
// Preserve an in-flight flash animation across the swap (scrollToLine may
|
|
460
|
+
// have landed while we were highlighting)
|
|
461
|
+
const flashed = new Set(
|
|
462
|
+
[...codeWrapper.querySelectorAll('.preview-line.flash')].map(el => el.dataset.line)
|
|
463
|
+
);
|
|
464
|
+
const foldMap = new Map((folds || []).map(f => [f.start, f.end]));
|
|
465
|
+
|
|
466
|
+
const rendered = htmlLines.map((lineHtml, i) => {
|
|
467
|
+
const lineNum = i + 1;
|
|
468
|
+
const classes = ['preview-line'];
|
|
469
|
+
if (highlightSet.has(lineNum)) classes.push('highlighted');
|
|
470
|
+
if (lineRange) {
|
|
471
|
+
if (lineNum === lineRange.start) classes.push('range-start');
|
|
472
|
+
if (lineNum === lineRange.end) classes.push('range-end');
|
|
473
|
+
}
|
|
474
|
+
if (flashed.has(String(lineNum))) classes.push('flash');
|
|
475
|
+
const foldEnd = foldMap.get(lineNum);
|
|
476
|
+
const foldCell = foldEnd
|
|
477
|
+
? `<span class="preview-fold" data-fold-end="${foldEnd}"></span>`
|
|
478
|
+
: '<span class="preview-fold"></span>';
|
|
479
|
+
return `<div class="${classes.join(' ')}" data-line="${lineNum}"><span class="line-number">${lineNum}</span>${foldCell}<span class="line-content">${lineHtml || ' '}</span></div>`;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
codeWrapper.innerHTML = `<pre class="preview-code highlighted"><code class="language-${langLabel}">${rendered.join('')}</code></pre>`;
|
|
483
|
+
|
|
484
|
+
// Fold arrow clicks — delegated; wrapper survives highlight swaps within
|
|
485
|
+
// one body render, and re-renders re-run this whole function anyway.
|
|
486
|
+
if (!codeWrapper._foldBound) {
|
|
487
|
+
codeWrapper._foldBound = true;
|
|
488
|
+
codeWrapper.addEventListener('click', handleFoldClick);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Toggle a fold region: hides/shows the lines inside the range, mirroring
|
|
494
|
+
* the editor's fold gutter behavior. Expanding also resets any nested
|
|
495
|
+
* collapsed folds inside the range.
|
|
496
|
+
*/
|
|
497
|
+
function handleFoldClick(e) {
|
|
498
|
+
const btn = e.target.closest('.preview-fold[data-fold-end]');
|
|
499
|
+
if (!btn) return;
|
|
500
|
+
const lineEl = btn.closest('.preview-line');
|
|
501
|
+
if (!lineEl) return;
|
|
502
|
+
const end = parseInt(btn.dataset.foldEnd, 10);
|
|
503
|
+
const collapsed = lineEl.classList.toggle('fold-collapsed');
|
|
504
|
+
for (let el = lineEl.nextElementSibling; el; el = el.nextElementSibling) {
|
|
505
|
+
const n = parseInt(el.dataset.line, 10);
|
|
506
|
+
if (isNaN(n) || n > end) break;
|
|
507
|
+
el.classList.toggle('fold-hidden', collapsed);
|
|
508
|
+
if (!collapsed) el.classList.remove('fold-collapsed');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Scroll to a specific line
|
|
514
|
+
* @param {Element} container - Widget container
|
|
515
|
+
* @param {number} lineNum - Line number to scroll to
|
|
516
|
+
* @param {object} options - Options
|
|
517
|
+
* @param {boolean} options.flash - Add flash animation (default: true)
|
|
518
|
+
* @param {string} options.position - 'center' or 'top' (default: 'center')
|
|
519
|
+
*/
|
|
520
|
+
export function scrollToLine(container, lineNum, options = {}) {
|
|
521
|
+
const { flash = true, position = 'center' } = options;
|
|
522
|
+
// Code/edit views: line-marked DOM. Rendered views (e.g. markdown):
|
|
523
|
+
// map line → source byte offset → block tagged with data-source-start/end.
|
|
524
|
+
const target = container.querySelector(`.preview-line[data-line="${lineNum}"]`)
|
|
525
|
+
|| findRenderedBlockForLine(container, lineNum);
|
|
526
|
+
if (!target) return;
|
|
527
|
+
|
|
528
|
+
const scrollContainer = container.querySelector('.preview-body');
|
|
529
|
+
|
|
530
|
+
if (position === 'top' && scrollContainer) {
|
|
531
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
532
|
+
const lineRect = target.getBoundingClientRect();
|
|
533
|
+
const currentScrollTop = scrollContainer.scrollTop;
|
|
534
|
+
const lineOffset = lineRect.top - containerRect.top + currentScrollTop;
|
|
535
|
+
scrollContainer.scrollTop = lineOffset;
|
|
536
|
+
} else {
|
|
537
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (flash) {
|
|
541
|
+
target.classList.add('flash');
|
|
542
|
+
setTimeout(() => target.classList.remove('flash'), 1500);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function findRenderedBlockForLine(container, lineNum) {
|
|
547
|
+
const rendered = container.querySelector('.preview-rendered');
|
|
548
|
+
if (!rendered) return null;
|
|
549
|
+
|
|
550
|
+
const content = state.content || '';
|
|
551
|
+
let offset = 0;
|
|
552
|
+
let line = 1;
|
|
553
|
+
while (line < lineNum && offset < content.length) {
|
|
554
|
+
const nl = content.indexOf('\n', offset);
|
|
555
|
+
if (nl === -1) break;
|
|
556
|
+
offset = nl + 1;
|
|
557
|
+
line++;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
let best = null;
|
|
561
|
+
let bestStart = -1;
|
|
562
|
+
for (const el of rendered.querySelectorAll('[data-source-start]')) {
|
|
563
|
+
const start = parseInt(el.getAttribute('data-source-start'), 10);
|
|
564
|
+
const end = parseInt(el.getAttribute('data-source-end'), 10);
|
|
565
|
+
if (offset >= start && offset <= end) return el;
|
|
566
|
+
if (start <= offset && start > bestStart) {
|
|
567
|
+
best = el;
|
|
568
|
+
bestStart = start;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return best;
|
|
572
|
+
}
|