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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Right-click / long-press context menu for JSON tree nodes.
|
|
3
|
+
*
|
|
4
|
+
* The tree is rendered as an HTML string, so the menu can't hold a reference to
|
|
5
|
+
* the value it was opened on — it resolves the clicked element's
|
|
6
|
+
* `data-json-path` back through the parsed document (resolveJsonPath). That
|
|
7
|
+
* keeps the renderer pure and means the same code serves the JSON preview, the
|
|
8
|
+
* JSONL preview (one document per row) and any future caller.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { copyToClipboard, showToast } from '../context-menu.js';
|
|
12
|
+
import { resolveJsonPath, formatJsonPath } from './json-tree.js';
|
|
13
|
+
import S from '../strings.js';
|
|
14
|
+
|
|
15
|
+
const LONG_PRESS_MS = 500;
|
|
16
|
+
const LONG_PRESS_SLOP = 10;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {Element} target - the tree root to delegate from ('.json-tree' / '.jsonl-rows')
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {(path: string) => *} opts.getRoot - parsed document a path is rooted in
|
|
22
|
+
* @param {(text: string) => void} [opts.onSearch] - fill the search box with this text
|
|
23
|
+
* @param {(path: string, expanded: boolean) => void} [opts.onSetExpanded] - record one node's state
|
|
24
|
+
* @param {() => void} [opts.onAfterExpand] - called once after a bulk expand/collapse
|
|
25
|
+
*/
|
|
26
|
+
export function bindJsonTreeContextMenu(target, opts = {}) {
|
|
27
|
+
const open = (x, y, el) => {
|
|
28
|
+
const node = el?.closest?.('[data-json-path]');
|
|
29
|
+
if (!node || !target.contains(node)) return false;
|
|
30
|
+
const items = buildItems(node, target, opts);
|
|
31
|
+
if (!items.length) return false;
|
|
32
|
+
const menu = window.app?.contextMenu;
|
|
33
|
+
if (!menu) return false;
|
|
34
|
+
menu.show(x, y, items);
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
target.addEventListener('contextmenu', (e) => {
|
|
39
|
+
// Let the browser's own menu handle a real text selection — that's the
|
|
40
|
+
// one case where the native "Copy" is more useful than ours.
|
|
41
|
+
if (!window.getSelection?.().isCollapsed) return;
|
|
42
|
+
if (open(e.clientX, e.clientY, e.target)) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
e.stopPropagation();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Touch long-press. iOS synthesizes a click after touchend, which would
|
|
49
|
+
// toggle the node the menu just opened on, so it gets swallowed.
|
|
50
|
+
let timer = null;
|
|
51
|
+
let startX = 0;
|
|
52
|
+
let startY = 0;
|
|
53
|
+
let fired = false;
|
|
54
|
+
|
|
55
|
+
const cancel = () => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
target.addEventListener('touchstart', (e) => {
|
|
61
|
+
if (e.touches.length !== 1) return cancel();
|
|
62
|
+
const t = e.touches[0];
|
|
63
|
+
startX = t.clientX;
|
|
64
|
+
startY = t.clientY;
|
|
65
|
+
fired = false;
|
|
66
|
+
const el = e.target;
|
|
67
|
+
timer = setTimeout(() => {
|
|
68
|
+
timer = null;
|
|
69
|
+
fired = open(startX, startY, el);
|
|
70
|
+
}, LONG_PRESS_MS);
|
|
71
|
+
}, { passive: true });
|
|
72
|
+
|
|
73
|
+
target.addEventListener('touchmove', (e) => {
|
|
74
|
+
const t = e.touches[0];
|
|
75
|
+
if (!t) return;
|
|
76
|
+
if (Math.abs(t.clientX - startX) > LONG_PRESS_SLOP
|
|
77
|
+
|| Math.abs(t.clientY - startY) > LONG_PRESS_SLOP) cancel();
|
|
78
|
+
}, { passive: true });
|
|
79
|
+
|
|
80
|
+
target.addEventListener('touchend', cancel, { passive: true });
|
|
81
|
+
target.addEventListener('touchcancel', cancel, { passive: true });
|
|
82
|
+
|
|
83
|
+
target.addEventListener('click', (e) => {
|
|
84
|
+
if (!fired) return;
|
|
85
|
+
fired = false;
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
}, { capture: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Menu contents ───────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function buildItems(node, target, opts) {
|
|
94
|
+
const M = S.preview?.json_menu || {};
|
|
95
|
+
const path = node.dataset.jsonPath;
|
|
96
|
+
const root = opts.getRoot?.(path);
|
|
97
|
+
const resolved = root === undefined ? { found: false } : resolveJsonPath(root, path);
|
|
98
|
+
const items = [];
|
|
99
|
+
|
|
100
|
+
if (resolved.found) {
|
|
101
|
+
const { value, key } = resolved;
|
|
102
|
+
const pretty = safeStringify(value);
|
|
103
|
+
const isScalar = value === null || typeof value !== 'object';
|
|
104
|
+
// For a string, "value" means the string itself — copying `"foo"` with
|
|
105
|
+
// the quotes baked in is almost never what you wanted.
|
|
106
|
+
const plain = typeof value === 'string' ? value
|
|
107
|
+
: isScalar ? String(value)
|
|
108
|
+
: pretty;
|
|
109
|
+
|
|
110
|
+
items.push({
|
|
111
|
+
label: M.copy_value || 'Copy value',
|
|
112
|
+
icon: 'copy',
|
|
113
|
+
action: () => copy(plain, M.copied_value || 'Value copied'),
|
|
114
|
+
});
|
|
115
|
+
if (pretty !== null && pretty !== plain) {
|
|
116
|
+
items.push({
|
|
117
|
+
label: M.copy_json || 'Copy as JSON',
|
|
118
|
+
icon: 'code',
|
|
119
|
+
action: () => copy(pretty, M.copied_json || 'JSON copied'),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (key) {
|
|
123
|
+
items.push({
|
|
124
|
+
label: M.copy_key || 'Copy key',
|
|
125
|
+
action: () => copy(key, M.copied_key || 'Key copied'),
|
|
126
|
+
});
|
|
127
|
+
if (isScalar) {
|
|
128
|
+
items.push({
|
|
129
|
+
label: M.copy_pair || 'Copy "key": value',
|
|
130
|
+
action: () => copy(`${JSON.stringify(key)}: ${safeStringify(value, 0)}`,
|
|
131
|
+
M.copied_pair || 'Entry copied'),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
items.push({
|
|
136
|
+
label: M.copy_path || 'Copy path',
|
|
137
|
+
action: () => copy(formatJsonPath(path), M.copied_path || 'Path copied'),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (isScalar && value !== null && opts.onSearch) {
|
|
141
|
+
const term = String(value);
|
|
142
|
+
if (term && term.length <= 200) {
|
|
143
|
+
items.push({ type: 'separator' });
|
|
144
|
+
items.push({
|
|
145
|
+
label: M.search_value || 'Search for this value',
|
|
146
|
+
icon: 'search',
|
|
147
|
+
action: () => opts.onSearch(term),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Subtree expand/collapse — only meaningful where there's something to fold.
|
|
154
|
+
const subtree = node.classList.contains('json-collapsible')
|
|
155
|
+
? node
|
|
156
|
+
: node.querySelector('.json-collapsible');
|
|
157
|
+
if (subtree) {
|
|
158
|
+
items.push({ type: 'separator' });
|
|
159
|
+
items.push({
|
|
160
|
+
label: M.expand_subtree || 'Expand subtree',
|
|
161
|
+
icon: 'chevronDown',
|
|
162
|
+
action: () => setSubtree(node, true, opts),
|
|
163
|
+
});
|
|
164
|
+
items.push({
|
|
165
|
+
label: M.collapse_subtree || 'Collapse subtree',
|
|
166
|
+
icon: 'chevronRight',
|
|
167
|
+
action: () => setSubtree(node, false, opts),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (root !== undefined) {
|
|
172
|
+
const whole = safeStringify(root);
|
|
173
|
+
if (whole !== null) {
|
|
174
|
+
items.push({ type: 'separator' });
|
|
175
|
+
items.push({
|
|
176
|
+
label: opts.rootLabel || M.copy_document || 'Copy whole document',
|
|
177
|
+
icon: 'file',
|
|
178
|
+
action: () => copy(whole, opts.rootToast || M.copied_document || 'Document copied'),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Fold or unfold everything under `node` in one shot: the DOM classes AND the
|
|
188
|
+
* override records, so the next re-render (any keystroke in the search box)
|
|
189
|
+
* doesn't undo it.
|
|
190
|
+
*/
|
|
191
|
+
function setSubtree(node, expanded, opts) {
|
|
192
|
+
const nodes = [];
|
|
193
|
+
if (node.classList.contains('json-collapsible')) nodes.push(node);
|
|
194
|
+
nodes.push(...node.querySelectorAll('.json-collapsible'));
|
|
195
|
+
for (const el of nodes) {
|
|
196
|
+
el.classList.toggle('expanded', expanded);
|
|
197
|
+
opts.onSetExpanded?.(el.dataset.jsonPath, expanded);
|
|
198
|
+
}
|
|
199
|
+
for (const el of node.querySelectorAll('.json-nested-container')) {
|
|
200
|
+
const nested = el.querySelector('.json-nested-content');
|
|
201
|
+
const preview = el.querySelector('.json-string-preview');
|
|
202
|
+
const btn = el.querySelector('.json-parse-btn');
|
|
203
|
+
if (!nested) continue;
|
|
204
|
+
nested.hidden = !expanded;
|
|
205
|
+
if (preview) preview.hidden = expanded;
|
|
206
|
+
btn?.classList.toggle('active', expanded);
|
|
207
|
+
opts.onSetExpanded?.(el.dataset.jsonPath, expanded);
|
|
208
|
+
}
|
|
209
|
+
opts.onAfterExpand?.();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** JSON.stringify that survives circular refs / BigInt rather than throwing mid-menu. */
|
|
213
|
+
function safeStringify(value, indent = 2) {
|
|
214
|
+
try {
|
|
215
|
+
const out = JSON.stringify(value, null, indent);
|
|
216
|
+
return out === undefined ? null : out;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function copy(text, toast) {
|
|
223
|
+
copyToClipboard(text).then(ok => {
|
|
224
|
+
showToast(ok ? toast : (S.preview?.json_menu?.copy_failed || 'Copy failed'));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared search / expand toolbar for the JSON and JSONL tree previews.
|
|
3
|
+
*
|
|
4
|
+
* Both plugins render the same control row and need the same behavior, so it
|
|
5
|
+
* lives here instead of being copy-pasted twice with two sets of bugs.
|
|
6
|
+
*
|
|
7
|
+
* What it owns:
|
|
8
|
+
* - the query, debounced into a re-render (the tree is a render-to-string
|
|
9
|
+
* module, so search *is* a re-render — see json-tree.js)
|
|
10
|
+
* - match count + prev/next navigation. The tree search used to highlight and
|
|
11
|
+
* nothing else: no count, no way to jump, Enter did nothing, and Ctrl+F
|
|
12
|
+
* handed you an input with none of the affordances the generic preview
|
|
13
|
+
* search bar has.
|
|
14
|
+
* - caret/focus survival across those re-renders
|
|
15
|
+
* - expand-all / collapse-all and per-node toggles as *state*, so the next
|
|
16
|
+
* keystroke doesn't throw them away
|
|
17
|
+
*
|
|
18
|
+
* The count is read back out of the DOM rather than tallied during render:
|
|
19
|
+
* whatever is highlighted and visible on screen is by definition what "N of M"
|
|
20
|
+
* has to mean, and there's no second bookkeeping path to drift out of sync.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { escapeHtml } from '../utils.js';
|
|
24
|
+
import { collectVisibleMatches } from './json-tree.js';
|
|
25
|
+
import { fns } from './preview-state.js';
|
|
26
|
+
import S from '../strings.js';
|
|
27
|
+
|
|
28
|
+
const DEBOUNCE_MS = 120;
|
|
29
|
+
|
|
30
|
+
export function initJsonToolbarState() {
|
|
31
|
+
return {
|
|
32
|
+
search: '',
|
|
33
|
+
// path → explicit expand state from a user toggle, so a re-render (i.e.
|
|
34
|
+
// every keystroke in the search box) doesn't undo it. Only expand-all /
|
|
35
|
+
// collapse-all resets the set.
|
|
36
|
+
overrides: new Map(),
|
|
37
|
+
// null | 'expand' | 'collapse' — sticky expand-all / collapse-all
|
|
38
|
+
mode: null,
|
|
39
|
+
matchIndex: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Tolerate a pluginState that was reset to a bare {} elsewhere. */
|
|
44
|
+
export function normalizeToolbarState(ps) {
|
|
45
|
+
if (!ps) return initJsonToolbarState();
|
|
46
|
+
if (typeof ps.search !== 'string') ps.search = '';
|
|
47
|
+
if (!(ps.overrides instanceof Map)) ps.overrides = new Map();
|
|
48
|
+
if (ps.mode !== 'expand' && ps.mode !== 'collapse') ps.mode = null;
|
|
49
|
+
if (typeof ps.matchIndex !== 'number' || ps.matchIndex < 0) ps.matchIndex = 0;
|
|
50
|
+
return ps;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Render options for the tree itself, derived from toolbar state. */
|
|
54
|
+
export function treeOptions(ps, extra = {}) {
|
|
55
|
+
return {
|
|
56
|
+
search: (ps.search || '').trim().toLowerCase(),
|
|
57
|
+
expandAll: ps.mode === 'expand',
|
|
58
|
+
collapseAll: ps.mode === 'collapse',
|
|
59
|
+
overrides: ps.overrides,
|
|
60
|
+
...extra,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function renderJsonToolbar(ps) {
|
|
65
|
+
const query = (ps.search || '').trim();
|
|
66
|
+
const hide = query ? '' : ' hidden';
|
|
67
|
+
const placeholder = S.preview?.json_search_placeholder || 'Search keys and values…';
|
|
68
|
+
|
|
69
|
+
return `
|
|
70
|
+
<div class="json-toolbar">
|
|
71
|
+
<div class="json-search-wrap">
|
|
72
|
+
<svg class="json-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
73
|
+
<circle cx="11" cy="11" r="8"/>
|
|
74
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
75
|
+
</svg>
|
|
76
|
+
<input type="text" class="json-search-input" placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(ps.search || '')}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
|
77
|
+
<span class="json-search-count"${hide}></span>
|
|
78
|
+
<button class="json-search-nav" data-action="search-prev" data-tooltip="${escapeHtml(S.preview?.json_search_prev || 'Previous match (Shift+Enter)')}"${hide}>
|
|
79
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
|
|
80
|
+
</button>
|
|
81
|
+
<button class="json-search-nav" data-action="search-next" data-tooltip="${escapeHtml(S.preview?.json_search_next || 'Next match (Enter)')}"${hide}>
|
|
82
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
83
|
+
</button>
|
|
84
|
+
<button class="json-search-clear" data-action="clear-search" data-tooltip="${escapeHtml(S.preview?.json_search_clear || 'Clear (Escape)')}"${hide}>×</button>
|
|
85
|
+
</div>
|
|
86
|
+
<button class="json-ctrl-btn ${ps.mode === 'expand' ? 'active' : ''}" data-action="expand-all" data-tooltip="${escapeHtml(S.preview?.json_expand_all || 'Expand all')}">
|
|
87
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
88
|
+
</button>
|
|
89
|
+
<button class="json-ctrl-btn ${ps.mode === 'collapse' ? 'active' : ''}" data-action="collapse-all" data-tooltip="${escapeHtml(S.preview?.json_collapse_all || 'Collapse all')}">
|
|
90
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wire the toolbar. Call once per render from the plugin's setupEvents.
|
|
98
|
+
*
|
|
99
|
+
* @param {Element} container - the preview widget container
|
|
100
|
+
* @param {object} ps - plugin state (see initJsonToolbarState)
|
|
101
|
+
* @param {object} helpers - plugin helpers ({ rerenderContent })
|
|
102
|
+
* @param {string} treeSelector - root the matches live under ('.json-tree', '.jsonl-rows')
|
|
103
|
+
*/
|
|
104
|
+
export function bindJsonToolbar(container, ps, helpers, treeSelector) {
|
|
105
|
+
normalizeToolbarState(ps);
|
|
106
|
+
|
|
107
|
+
const refresh = (opts) => refreshMatches(container, ps, treeSelector, opts);
|
|
108
|
+
const rerender = (scroll) => {
|
|
109
|
+
const input = liveContainer(container).querySelector('.json-search-input');
|
|
110
|
+
const focused = input && document.activeElement === input;
|
|
111
|
+
const selStart = focused ? input.selectionStart : null;
|
|
112
|
+
const selEnd = focused ? input.selectionEnd : null;
|
|
113
|
+
helpers.rerenderContent();
|
|
114
|
+
if (focused) restoreSearchFocus(container, ps, selStart, selEnd);
|
|
115
|
+
refresh({ scroll });
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const applyQuery = (value) => {
|
|
119
|
+
if (value === ps.search) return false;
|
|
120
|
+
ps.search = value;
|
|
121
|
+
// New query, new match set — start navigation at the top. Manual
|
|
122
|
+
// expand/collapse overrides are deliberately KEPT (they outlive a
|
|
123
|
+
// re-render); auto-reveal outranks them anyway, see resolveExpanded.
|
|
124
|
+
ps.matchIndex = 0;
|
|
125
|
+
rerender(true);
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const searchInput = container.querySelector('.json-search-input');
|
|
130
|
+
let debounce = null;
|
|
131
|
+
const flush = () => {
|
|
132
|
+
clearTimeout(debounce);
|
|
133
|
+
debounce = null;
|
|
134
|
+
const input = liveContainer(container).querySelector('.json-search-input');
|
|
135
|
+
return input ? applyQuery(input.value) : false;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (searchInput) {
|
|
139
|
+
searchInput.addEventListener('input', () => {
|
|
140
|
+
clearTimeout(debounce);
|
|
141
|
+
debounce = setTimeout(flush, DEBOUNCE_MS);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
145
|
+
if (e.key === 'Enter') {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
// Enter straight after typing must search what's in the box,
|
|
148
|
+
// not what the debounce has committed so far.
|
|
149
|
+
if (!flush()) navigate(container, ps, treeSelector, e.shiftKey ? -1 : 1);
|
|
150
|
+
} else if (e.key === 'Escape') {
|
|
151
|
+
// Don't let Escape bubble out and close the whole preview.
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
clearTimeout(debounce);
|
|
155
|
+
if (searchInput.value) {
|
|
156
|
+
searchInput.value = '';
|
|
157
|
+
applyQuery('');
|
|
158
|
+
} else {
|
|
159
|
+
searchInput.blur();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
container.querySelectorAll('.json-ctrl-btn, .json-search-nav, .json-search-clear').forEach(btn => {
|
|
166
|
+
btn.addEventListener('click', () => {
|
|
167
|
+
const act = btn.dataset.action;
|
|
168
|
+
if (act === 'expand-all' || act === 'collapse-all') {
|
|
169
|
+
// State, not a DOM sweep: the old version toggled classes
|
|
170
|
+
// directly, so the very next re-render (a keystroke, a poll)
|
|
171
|
+
// silently undid it.
|
|
172
|
+
const next = act === 'expand-all' ? 'expand' : 'collapse';
|
|
173
|
+
ps.mode = ps.mode === next ? null : next;
|
|
174
|
+
ps.overrides = new Map();
|
|
175
|
+
rerender(false);
|
|
176
|
+
} else if (act === 'clear-search') {
|
|
177
|
+
clearTimeout(debounce);
|
|
178
|
+
applyQuery('');
|
|
179
|
+
} else if (act === 'search-prev') {
|
|
180
|
+
navigate(container, ps, treeSelector, -1);
|
|
181
|
+
} else if (act === 'search-next') {
|
|
182
|
+
navigate(container, ps, treeSelector, 1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const record = (path, expanded) => {
|
|
188
|
+
if (path) ps.overrides.set(path, expanded);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
onToggle(path, expanded) {
|
|
193
|
+
record(path, expanded);
|
|
194
|
+
// Folding/unfolding changes which matches are on screen.
|
|
195
|
+
refresh({ scroll: false });
|
|
196
|
+
},
|
|
197
|
+
// Batched variant for the context menu's expand/collapse-subtree, which
|
|
198
|
+
// touches many nodes and wants a single recount at the end.
|
|
199
|
+
record,
|
|
200
|
+
refresh,
|
|
201
|
+
/** Drive the search box from elsewhere (e.g. "Search for this value"). */
|
|
202
|
+
setQuery(value) {
|
|
203
|
+
clearTimeout(debounce);
|
|
204
|
+
const input = liveContainer(container).querySelector('.json-search-input');
|
|
205
|
+
if (input) input.value = value;
|
|
206
|
+
if (!applyQuery(value)) refresh({ scroll: true });
|
|
207
|
+
liveContainer(container).querySelector('.json-search-input')?.focus();
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Internal helpers ────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A re-render can swap the container node out from under us (the floating
|
|
216
|
+
* widget path rebuilds it); fall back to the live instance lookup rather than
|
|
217
|
+
* writing into a detached tree.
|
|
218
|
+
*/
|
|
219
|
+
function liveContainer(container) {
|
|
220
|
+
if (container?.isConnected) return container;
|
|
221
|
+
return fns.findPreviewContainer?.() || container;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function restoreSearchFocus(container, ps, selStart, selEnd) {
|
|
225
|
+
const apply = (guard) => {
|
|
226
|
+
const input = liveContainer(container).querySelector('.json-search-input');
|
|
227
|
+
if (!input) return;
|
|
228
|
+
// Never clobber a caret the user has moved on with since the re-render.
|
|
229
|
+
if (guard && input.value !== ps.search) return;
|
|
230
|
+
if (document.activeElement !== input) input.focus();
|
|
231
|
+
const end = input.value.length;
|
|
232
|
+
try {
|
|
233
|
+
input.setSelectionRange(Math.min(selStart ?? end, end), Math.min(selEnd ?? end, end));
|
|
234
|
+
} catch (e) { /* input type without selection support */ }
|
|
235
|
+
};
|
|
236
|
+
apply(false);
|
|
237
|
+
requestAnimationFrame(() => apply(true));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function refreshMatches(container, ps, treeSelector, { scroll = false } = {}) {
|
|
241
|
+
const root = liveContainer(container);
|
|
242
|
+
const treeRoot = root.querySelector(treeSelector);
|
|
243
|
+
const matches = collectVisibleMatches(treeRoot);
|
|
244
|
+
const query = (ps.search || '').trim();
|
|
245
|
+
|
|
246
|
+
ps.matchIndex = matches.length
|
|
247
|
+
? Math.min(Math.max(ps.matchIndex | 0, 0), matches.length - 1)
|
|
248
|
+
: 0;
|
|
249
|
+
|
|
250
|
+
root.querySelectorAll('.json-match.current').forEach(el => el.classList.remove('current'));
|
|
251
|
+
const current = matches[ps.matchIndex];
|
|
252
|
+
if (current) current.classList.add('current');
|
|
253
|
+
|
|
254
|
+
const countEl = root.querySelector('.json-search-count');
|
|
255
|
+
if (countEl) {
|
|
256
|
+
countEl.hidden = !query;
|
|
257
|
+
countEl.textContent = matches.length
|
|
258
|
+
? `${ps.matchIndex + 1} of ${matches.length}`
|
|
259
|
+
: (query ? (S.preview?.json_search_no_results || 'No results') : '');
|
|
260
|
+
countEl.classList.toggle('no-results', !!query && matches.length === 0);
|
|
261
|
+
}
|
|
262
|
+
root.querySelectorAll('.json-search-nav').forEach(btn => {
|
|
263
|
+
btn.hidden = !query;
|
|
264
|
+
btn.disabled = matches.length === 0;
|
|
265
|
+
});
|
|
266
|
+
const clearBtn = root.querySelector('.json-search-clear');
|
|
267
|
+
if (clearBtn) clearBtn.hidden = !query;
|
|
268
|
+
|
|
269
|
+
if (scroll && current) scrollMatchIntoView(root, current);
|
|
270
|
+
return matches;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function navigate(container, ps, treeSelector, direction) {
|
|
274
|
+
const matches = refreshMatches(container, ps, treeSelector);
|
|
275
|
+
if (!matches.length) return;
|
|
276
|
+
ps.matchIndex = (ps.matchIndex + direction + matches.length) % matches.length;
|
|
277
|
+
refreshMatches(container, ps, treeSelector, { scroll: true });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Manual scrollTop math rather than scrollIntoView: iOS WKWebView ignores
|
|
282
|
+
* `block:'center'` on overflow:auto containers, which is the same reason
|
|
283
|
+
* preview-search.js does it by hand.
|
|
284
|
+
*/
|
|
285
|
+
function scrollMatchIntoView(container, el) {
|
|
286
|
+
const scroller = container.querySelector('.preview-body') || container;
|
|
287
|
+
const containerRect = scroller.getBoundingClientRect();
|
|
288
|
+
const matchRect = el.getBoundingClientRect();
|
|
289
|
+
const offset = matchRect.top - containerRect.top + scroller.scrollTop;
|
|
290
|
+
scroller.scrollTop = Math.max(0, offset - scroller.clientHeight / 2 + matchRect.height / 2);
|
|
291
|
+
}
|