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,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App API Routes - residual: server info, global config, helper install.
|
|
3
|
+
|
|
4
|
+
These endpoints expose:
|
|
5
|
+
- Server identity (home, cwd, workspace)
|
|
6
|
+
- Global configuration (raw get/put)
|
|
7
|
+
- Helper installation (shadow-git CLI + agent template)
|
|
8
|
+
|
|
9
|
+
Other former sections of this module have moved:
|
|
10
|
+
- Quick action presets, claude path, max thinking tokens, API auto-retry,
|
|
11
|
+
default permissions, token profiles, models, default effort, session
|
|
12
|
+
timeouts -> routes/api_app_config.py
|
|
13
|
+
- Tab state, keyboard shortcuts, user snippets, agent patterns, shadow-git
|
|
14
|
+
defaults -> routes/api_app_session_prefs.py
|
|
15
|
+
- Commit sections -> routes/api_app_commit_sections.py
|
|
16
|
+
- Project config, project rename, project commands (and the CLI command
|
|
17
|
+
description cache used by routes/api_commands.py) -> routes/api_project_config.py
|
|
18
|
+
- Agent discovery -> routes/api_agents.py
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
27
|
+
|
|
28
|
+
from painapple_code import paths
|
|
29
|
+
from painapple_code import helpers as helpers_module
|
|
30
|
+
from painapple_code.utils.file_paths import safe_resolve
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
router = APIRouter(tags=["app"])
|
|
35
|
+
|
|
36
|
+
# Pinned at boot by capture_boot_version(); this is the code the process
|
|
37
|
+
# actually imported. Never recomputed — that's the whole point.
|
|
38
|
+
_boot_version: dict | None = None
|
|
39
|
+
|
|
40
|
+
# Live worktree reading, refreshed at most every _DISK_TTL seconds.
|
|
41
|
+
_disk_version: tuple[float, str | None] | None = None
|
|
42
|
+
_DISK_TTL = 10.0
|
|
43
|
+
|
|
44
|
+
# Distribution metadata (license + project URLs), read once on first use.
|
|
45
|
+
_package_meta: dict | None = None
|
|
46
|
+
|
|
47
|
+
# Fallbacks for when distribution metadata is unavailable — running from a
|
|
48
|
+
# source tree that was never pip-installed leaves importlib.metadata empty.
|
|
49
|
+
# These mirror [project] / [project.urls] in pyproject.toml.
|
|
50
|
+
_META_FALLBACK = {
|
|
51
|
+
"license": "AGPL-3.0-or-later",
|
|
52
|
+
"author": "Michał Booth-Wrotkowski",
|
|
53
|
+
"urls": {
|
|
54
|
+
"Homepage": "https://painapple.ai",
|
|
55
|
+
"Source": "https://github.com/wrotek/painapple-code",
|
|
56
|
+
"Issues": "https://github.com/wrotek/painapple-code/issues",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_package_meta() -> dict:
|
|
62
|
+
"""License expression + project URLs, straight from the installed dist.
|
|
63
|
+
|
|
64
|
+
Read from distribution metadata rather than hardcoded in the frontend so
|
|
65
|
+
a FORK automatically advertises its OWN source URL. That matters legally,
|
|
66
|
+
not just cosmetically: painapple-code is AGPL-3.0-or-later, and §13
|
|
67
|
+
obliges whoever operates it over a network to offer users the source of
|
|
68
|
+
the version they're actually talking to. Someone who forks, edits, and
|
|
69
|
+
redeploys inherits that duty — pointing at upstream's repo would be a
|
|
70
|
+
false offer. Editing pyproject.toml is the one place they'd naturally
|
|
71
|
+
change, so it's the one place we read.
|
|
72
|
+
"""
|
|
73
|
+
global _package_meta
|
|
74
|
+
if _package_meta is not None:
|
|
75
|
+
return _package_meta
|
|
76
|
+
|
|
77
|
+
meta = {"license": None, "author": None, "urls": {}}
|
|
78
|
+
try:
|
|
79
|
+
from email.utils import parseaddr
|
|
80
|
+
from importlib.metadata import metadata
|
|
81
|
+
|
|
82
|
+
md = metadata("painapple-code")
|
|
83
|
+
# PEP 639 SPDX expression; older setuptools emits a Classifier
|
|
84
|
+
# instead, so fall back to the plain License field.
|
|
85
|
+
meta["license"] = md.get("License-Expression") or md.get("License")
|
|
86
|
+
# A pyproject `authors = [{name, email}]` entry is flattened into
|
|
87
|
+
# Author-email as "Name <addr>". Take the display name only — the
|
|
88
|
+
# address has no business being published to every client, and
|
|
89
|
+
# scraping a support inbox out of an About box is free.
|
|
90
|
+
name, _addr = parseaddr(md.get("Author-email") or "")
|
|
91
|
+
meta["author"] = (name or md.get("Author") or "").strip() or None
|
|
92
|
+
# Project-URL is a multi-value field, "Label, https://…" per entry.
|
|
93
|
+
for entry in md.get_all("Project-URL") or []:
|
|
94
|
+
label, _, url = entry.partition(",")
|
|
95
|
+
if url.strip():
|
|
96
|
+
meta["urls"][label.strip()] = url.strip()
|
|
97
|
+
except Exception:
|
|
98
|
+
logger.debug("package metadata unavailable; using pyproject fallback")
|
|
99
|
+
|
|
100
|
+
if not meta["license"]:
|
|
101
|
+
meta["license"] = _META_FALLBACK["license"]
|
|
102
|
+
if not meta["author"]:
|
|
103
|
+
meta["author"] = _META_FALLBACK["author"]
|
|
104
|
+
if not meta["urls"]:
|
|
105
|
+
meta["urls"] = dict(_META_FALLBACK["urls"])
|
|
106
|
+
|
|
107
|
+
_package_meta = meta
|
|
108
|
+
return _package_meta
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _describe_checkout() -> str | None:
|
|
112
|
+
"""`git describe` the source tree we're importing from, or None.
|
|
113
|
+
|
|
114
|
+
Only meaningful for an editable install, where REPO_ROOT is a real
|
|
115
|
+
checkout and the files on disk ARE the running code. Wheel/Docker
|
|
116
|
+
installs land in site-packages with no .git, and return None.
|
|
117
|
+
"""
|
|
118
|
+
from painapple_code import REPO_ROOT
|
|
119
|
+
|
|
120
|
+
if not (REPO_ROOT / ".git").exists():
|
|
121
|
+
return None
|
|
122
|
+
try:
|
|
123
|
+
out = subprocess.run(
|
|
124
|
+
# Same tag filter as [tool.setuptools_scm] in pyproject.toml,
|
|
125
|
+
# so this agrees with what a rebuild would produce.
|
|
126
|
+
["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"],
|
|
127
|
+
cwd=str(REPO_ROOT), capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=2,
|
|
128
|
+
)
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
if out.returncode != 0:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
raw = out.stdout.strip() # v1.0.0-rc25-3-gabc1234[-dirty]
|
|
135
|
+
dirty = raw.endswith("-dirty")
|
|
136
|
+
if dirty:
|
|
137
|
+
raw = raw[: -len("-dirty")]
|
|
138
|
+
parts = raw.rsplit("-", 2)
|
|
139
|
+
if len(parts) != 3:
|
|
140
|
+
return None
|
|
141
|
+
tag, distance, _hash = parts
|
|
142
|
+
|
|
143
|
+
label = tag
|
|
144
|
+
if distance.isdigit() and int(distance) > 0:
|
|
145
|
+
label += f" +{distance}"
|
|
146
|
+
if dirty:
|
|
147
|
+
label += " (modified)"
|
|
148
|
+
return label
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def capture_boot_version() -> dict:
|
|
152
|
+
"""Pin the running version, once, at server startup.
|
|
153
|
+
|
|
154
|
+
Three different things can be called "the version", and conflating
|
|
155
|
+
them is how this went wrong twice:
|
|
156
|
+
|
|
157
|
+
1. `__version__` from _version.py — setuptools-scm's BUILD-time
|
|
158
|
+
constant. Exact for a wheel or Docker image (the build IS the
|
|
159
|
+
install); frozen at the last `pip install -e .` for an editable
|
|
160
|
+
checkout. deploy.fish never reinstalls, so stable reported rc7
|
|
161
|
+
from July while serving rc25, 167 commits later.
|
|
162
|
+
2. `git describe` of the worktree — what's on DISK right now.
|
|
163
|
+
3. What this process actually imported — disk as it was at BOOT.
|
|
164
|
+
|
|
165
|
+
The About row wants (3). Reading (2) at request time silently
|
|
166
|
+
substitutes it for (3), which is wrong in the other direction: after
|
|
167
|
+
a fast-forward without a restart it reports the new code while
|
|
168
|
+
serving the old, hiding exactly the restart-needed case. So we snapshot
|
|
169
|
+
at startup, before anything can change underneath us, and diff against
|
|
170
|
+
(2) separately to detect that case.
|
|
171
|
+
"""
|
|
172
|
+
global _boot_version
|
|
173
|
+
from painapple_code import __version__
|
|
174
|
+
|
|
175
|
+
described = _describe_checkout()
|
|
176
|
+
_boot_version = (
|
|
177
|
+
{"version": described, "version_source": "git"} if described
|
|
178
|
+
else {"version": f"v{__version__}", "version_source": "package"}
|
|
179
|
+
)
|
|
180
|
+
# Deliberately NOT this module's `logger`: server_logging attaches its
|
|
181
|
+
# file handlers to the "painapple-code" logger (hyphen), while
|
|
182
|
+
# getLogger(__name__) lands under "painapple_code.routes" (underscore) —
|
|
183
|
+
# a separate tree with no handlers, so those records never reach
|
|
184
|
+
# server.log. Which version booted is exactly the line you want in the
|
|
185
|
+
# log when a deploy looks wrong, so log it where it'll actually appear.
|
|
186
|
+
logging.getLogger("painapple-code").info(
|
|
187
|
+
f"Running version: {_boot_version['version']} "
|
|
188
|
+
f"(source: {_boot_version['version_source']})"
|
|
189
|
+
)
|
|
190
|
+
return _boot_version
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _current_disk_version() -> str | None:
|
|
194
|
+
"""What `git describe` says about the worktree *now*, TTL-cached.
|
|
195
|
+
|
|
196
|
+
Differs from the boot snapshot exactly when the checkout moved under a
|
|
197
|
+
running process — i.e. a restart is pending.
|
|
198
|
+
"""
|
|
199
|
+
global _disk_version
|
|
200
|
+
import time
|
|
201
|
+
|
|
202
|
+
now = time.monotonic()
|
|
203
|
+
if _disk_version is None or (now - _disk_version[0]) > _DISK_TTL:
|
|
204
|
+
_disk_version = (now, _describe_checkout())
|
|
205
|
+
return _disk_version[1]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _version_payload() -> dict:
|
|
209
|
+
"""Running version, plus the on-disk version when a restart is pending."""
|
|
210
|
+
running = _boot_version if _boot_version is not None else capture_boot_version()
|
|
211
|
+
payload = dict(running)
|
|
212
|
+
|
|
213
|
+
# Only meaningful for a checkout — a wheel/Docker install can't change
|
|
214
|
+
# underneath the process, so there's nothing to compare against.
|
|
215
|
+
if running["version_source"] == "git":
|
|
216
|
+
disk = _current_disk_version()
|
|
217
|
+
if disk and disk != running["version"]:
|
|
218
|
+
payload["disk_version"] = disk
|
|
219
|
+
payload["restart_needed"] = True
|
|
220
|
+
return payload
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
224
|
+
# Server Info
|
|
225
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
@router.get("/api/info")
|
|
228
|
+
async def get_server_info(request: Request):
|
|
229
|
+
"""Server identity: OS home, server CWD, and the server's --workspace.
|
|
230
|
+
|
|
231
|
+
The frontend uses `workspace` (when present) as the project base for the
|
|
232
|
+
file explorer and path autocomplete — inside Docker the OS home is
|
|
233
|
+
/home/app, which is not what the user wants as their working area.
|
|
234
|
+
|
|
235
|
+
Reads workspace from app.state because `from server import agents` loads
|
|
236
|
+
a separate `server` module from the running `__main__`, where `agents` is
|
|
237
|
+
still None — app.state survives that split because the FastAPI app
|
|
238
|
+
object is the singleton used to register routes.
|
|
239
|
+
|
|
240
|
+
Also reports version identity, surfaced in the help dialog's About
|
|
241
|
+
section: `version` is what this process is RUNNING (pinned at boot —
|
|
242
|
+
see capture_boot_version) with `version_source` naming where it came
|
|
243
|
+
from, plus `disk_version`/`restart_needed` when the checkout has moved
|
|
244
|
+
on since. `static_build` is the newest static-asset mtime — the same
|
|
245
|
+
value used as the `?v=` cache-bust on /app, so the client can compare
|
|
246
|
+
it against the build it actually loaded and flag a stale page.
|
|
247
|
+
|
|
248
|
+
The two are the same idea one layer apart: loaded-vs-current assets
|
|
249
|
+
means reload, booted-vs-current code means restart.
|
|
250
|
+
|
|
251
|
+
`frontend_bundled`/`frontend_reason` report how those assets are being
|
|
252
|
+
delivered — one bundle or ~190 loose modules — because whether a source
|
|
253
|
+
install picked up the bundle depends on Node being present at install
|
|
254
|
+
time, which is invisible from the page and easy to get silently wrong.
|
|
255
|
+
|
|
256
|
+
`license` and `urls` come from the installed distribution's metadata and
|
|
257
|
+
feed the About widget's source-offer link — see _get_package_meta for
|
|
258
|
+
why they're read from the dist rather than hardcoded client-side.
|
|
259
|
+
"""
|
|
260
|
+
import os
|
|
261
|
+
|
|
262
|
+
raw_ws = getattr(request.app.state, "workspace", None)
|
|
263
|
+
workspace = None
|
|
264
|
+
if raw_ws:
|
|
265
|
+
try:
|
|
266
|
+
workspace = str(safe_resolve(raw_ws))
|
|
267
|
+
except Exception:
|
|
268
|
+
workspace = raw_ws
|
|
269
|
+
|
|
270
|
+
# Imported lazily: server.py imports this module at load time, so a
|
|
271
|
+
# module-level import would be circular. By request time it's cached.
|
|
272
|
+
try:
|
|
273
|
+
from painapple_code.server import _asset_version, frontend_delivery
|
|
274
|
+
static_build = _asset_version()
|
|
275
|
+
delivery = frontend_delivery()
|
|
276
|
+
except Exception:
|
|
277
|
+
static_build = None
|
|
278
|
+
delivery = {"bundled": None, "reason": None}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"home": str(Path.home()),
|
|
282
|
+
"cwd": os.getcwd(),
|
|
283
|
+
"workspace": workspace,
|
|
284
|
+
"static_build": static_build,
|
|
285
|
+
"frontend_bundled": delivery["bundled"],
|
|
286
|
+
"frontend_reason": delivery["reason"],
|
|
287
|
+
**_get_package_meta(),
|
|
288
|
+
**_version_payload(),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
293
|
+
# App Config
|
|
294
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
295
|
+
|
|
296
|
+
@router.get("/api/app/config")
|
|
297
|
+
async def get_app_config():
|
|
298
|
+
"""Get global configuration."""
|
|
299
|
+
return paths.load_global_config()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@router.put("/api/app/config")
|
|
303
|
+
async def update_app_config(config: dict):
|
|
304
|
+
"""Update global configuration."""
|
|
305
|
+
paths.save_global_config(config)
|
|
306
|
+
return paths.load_global_config()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
310
|
+
# Helper Install (shadow-git CLI + agent template)
|
|
311
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
312
|
+
|
|
313
|
+
@router.get("/api/app/helpers/status")
|
|
314
|
+
async def get_helpers_status():
|
|
315
|
+
"""Return install + freshness state for bundled helpers."""
|
|
316
|
+
return helpers_module.helpers_status()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@router.post("/api/app/helpers/install")
|
|
320
|
+
async def install_helpers():
|
|
321
|
+
"""
|
|
322
|
+
Run tools/install-helpers.sh --update on the server's filesystem.
|
|
323
|
+
Returns 200 with {ok, exit_code, stdout, stderr} regardless of script outcome
|
|
324
|
+
so the frontend can read the result without HTTP error handling.
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
# In-process now (helpers.install_helpers) rather than `bash
|
|
328
|
+
# tools/install-helpers.sh --update`: bash isn't present on a stock
|
|
329
|
+
# Windows box, and uninstall was already pure Python — the two
|
|
330
|
+
# halves of the same feature no longer disagree about their
|
|
331
|
+
# requirements. Still off the event loop: it's blocking file IO.
|
|
332
|
+
result = await asyncio.to_thread(helpers_module.install_helpers, True)
|
|
333
|
+
# The install overwrites the bundled agent file (model: sonnet
|
|
334
|
+
# default); re-apply the user's persisted subagent-model choice on top.
|
|
335
|
+
try:
|
|
336
|
+
helpers_module.apply_agent_model_from_config()
|
|
337
|
+
except Exception:
|
|
338
|
+
logger.exception("failed to re-apply helper agent model after install")
|
|
339
|
+
return result
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.exception("helper install failed to run")
|
|
342
|
+
return {"ok": False, "exit_code": -1, "stdout": "", "stderr": str(e)}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@router.post("/api/app/helpers/uninstall")
|
|
346
|
+
async def uninstall_helpers():
|
|
347
|
+
"""
|
|
348
|
+
Remove the installed helper files from the server's filesystem.
|
|
349
|
+
Does not touch journal data or shadow-git config — only deletes the
|
|
350
|
+
CLI binary and agent template that `install-helpers.sh` puts in place.
|
|
351
|
+
"""
|
|
352
|
+
return helpers_module.uninstall_helpers()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@router.post("/api/app/helpers/agent-model")
|
|
356
|
+
async def set_helper_agent_model(payload: dict):
|
|
357
|
+
"""Set the model the shadow-git-helper subagent runs on.
|
|
358
|
+
|
|
359
|
+
Persists the choice to the global config and applies it to the installed
|
|
360
|
+
agent file immediately when present. Body:
|
|
361
|
+
``{"model": "inherit|haiku|sonnet|opus"}``.
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
model = paths.set_helper_agent_model((payload or {}).get("model"))
|
|
365
|
+
except ValueError as e:
|
|
366
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
367
|
+
applied = helpers_module.apply_agent_model(model)
|
|
368
|
+
return {"ok": True, "model": model, "applied": applied}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Commit Sections API Routes.
|
|
3
|
+
|
|
4
|
+
Endpoints for managing commit message sections configuration for shadow git
|
|
5
|
+
on a per-project basis.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
11
|
+
|
|
12
|
+
from painapple_code import paths
|
|
13
|
+
from painapple_code.shadow_git import (
|
|
14
|
+
BUILTIN_SECTIONS, get_shadow_git, get_commit_sections_for_api
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
router = APIRouter(tags=["app:commit-sections"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
23
|
+
# Commit Sections API
|
|
24
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
@router.get("/api/app/projects/{project_hash}/commit-sections")
|
|
27
|
+
async def get_commit_sections(project_hash: str):
|
|
28
|
+
"""Get commit message sections configuration for a project."""
|
|
29
|
+
project_path = paths.get_project_path_from_hash(project_hash)
|
|
30
|
+
if not project_path:
|
|
31
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
32
|
+
|
|
33
|
+
project_config = paths.load_project_config(project_path)
|
|
34
|
+
return get_commit_sections_for_api(project_config)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.put("/api/app/projects/{project_hash}/commit-sections")
|
|
38
|
+
async def update_commit_sections(project_hash: str, request: Request):
|
|
39
|
+
"""Update commit sections configuration for a project."""
|
|
40
|
+
project_path = paths.get_project_path_from_hash(project_hash)
|
|
41
|
+
if not project_path:
|
|
42
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
43
|
+
|
|
44
|
+
body = await request.json()
|
|
45
|
+
sections_update = body.get("sections", {})
|
|
46
|
+
|
|
47
|
+
project_config = paths.load_project_config(project_path)
|
|
48
|
+
|
|
49
|
+
if "shadow_git" not in project_config:
|
|
50
|
+
project_config["shadow_git"] = {}
|
|
51
|
+
if "commit_sections" not in project_config["shadow_git"]:
|
|
52
|
+
project_config["shadow_git"]["commit_sections"] = {}
|
|
53
|
+
|
|
54
|
+
current_sections = project_config["shadow_git"]["commit_sections"]
|
|
55
|
+
|
|
56
|
+
for section_id, updates in sections_update.items():
|
|
57
|
+
if updates.get("delete") and section_id not in BUILTIN_SECTIONS:
|
|
58
|
+
current_sections.pop(section_id, None)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
if section_id in BUILTIN_SECTIONS:
|
|
62
|
+
builtin = BUILTIN_SECTIONS[section_id]
|
|
63
|
+
if section_id not in current_sections:
|
|
64
|
+
current_sections[section_id] = {}
|
|
65
|
+
|
|
66
|
+
if "enabled" in updates and not builtin["required"]:
|
|
67
|
+
current_sections[section_id]["enabled"] = bool(updates["enabled"])
|
|
68
|
+
if "order" in updates:
|
|
69
|
+
current_sections[section_id]["order"] = int(updates["order"])
|
|
70
|
+
if "prompt" in updates and not builtin["required"]:
|
|
71
|
+
current_sections[section_id]["prompt"] = str(updates["prompt"])
|
|
72
|
+
else:
|
|
73
|
+
if "title" not in updates or "prompt" not in updates:
|
|
74
|
+
if section_id not in current_sections:
|
|
75
|
+
raise HTTPException(
|
|
76
|
+
status_code=400,
|
|
77
|
+
detail=f"Custom section '{section_id}' requires 'title' and 'prompt'"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
current_sections[section_id] = {
|
|
81
|
+
"title": str(updates["title"]),
|
|
82
|
+
"prompt": str(updates["prompt"]),
|
|
83
|
+
"enabled": updates.get("enabled", True),
|
|
84
|
+
"order": updates.get("order", 55),
|
|
85
|
+
"applies_to": updates.get("applies_to", ["file_changes", "tool_only"]),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if section_id in current_sections:
|
|
89
|
+
if "enabled" in updates:
|
|
90
|
+
current_sections[section_id]["enabled"] = bool(updates["enabled"])
|
|
91
|
+
if "order" in updates:
|
|
92
|
+
current_sections[section_id]["order"] = int(updates["order"])
|
|
93
|
+
if "prompt" in updates:
|
|
94
|
+
current_sections[section_id]["prompt"] = str(updates["prompt"])
|
|
95
|
+
if "title" in updates:
|
|
96
|
+
current_sections[section_id]["title"] = str(updates["title"])
|
|
97
|
+
if "applies_to" in updates:
|
|
98
|
+
current_sections[section_id]["applies_to"] = updates["applies_to"]
|
|
99
|
+
|
|
100
|
+
paths.save_project_config(project_path, project_config)
|
|
101
|
+
logger.info(f"Updated commit sections for project {project_hash}")
|
|
102
|
+
|
|
103
|
+
shadow = get_shadow_git(project_path)
|
|
104
|
+
shadow.reload_config()
|
|
105
|
+
|
|
106
|
+
return get_commit_sections_for_api(project_config)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.post("/api/app/projects/{project_hash}/commit-sections/reset")
|
|
110
|
+
async def reset_commit_sections(project_hash: str):
|
|
111
|
+
"""Reset commit sections to defaults."""
|
|
112
|
+
project_path = paths.get_project_path_from_hash(project_hash)
|
|
113
|
+
if not project_path:
|
|
114
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
115
|
+
|
|
116
|
+
project_config = paths.load_project_config(project_path)
|
|
117
|
+
|
|
118
|
+
if "shadow_git" in project_config:
|
|
119
|
+
project_config["shadow_git"].pop("commit_sections", None)
|
|
120
|
+
|
|
121
|
+
paths.save_project_config(project_path, project_config)
|
|
122
|
+
logger.info(f"Reset commit sections to defaults for project {project_hash}")
|
|
123
|
+
|
|
124
|
+
shadow = get_shadow_git(project_path)
|
|
125
|
+
shadow.reload_config()
|
|
126
|
+
|
|
127
|
+
return get_commit_sections_for_api(project_config)
|