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,462 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shadow Git - summary fork for rich commit messages.
|
|
3
|
+
|
|
4
|
+
`ShadowGit.commit_turn` delegates to the methods in `_SummaryMixin` when the
|
|
5
|
+
project has rich commits enabled:
|
|
6
|
+
|
|
7
|
+
- `_build_journey_context` reads previous commits' YAML frontmatter and
|
|
8
|
+
filters by branch to assemble a per-session journey timeline.
|
|
9
|
+
- `_generate_rich_commit_message` is the heavy lifter: it asks the session's
|
|
10
|
+
provider how to fork itself (`build_summary_fork`), spawns that subprocess,
|
|
11
|
+
registers it for visibility in the Active Sessions widget, parses the
|
|
12
|
+
structured JSON response, and renders a markdown commit body.
|
|
13
|
+
- `_extract_title` mines a one-liner title out of the rendered body.
|
|
14
|
+
- `_update_session_meta` rolls per-turn cost/file/tool counters back into
|
|
15
|
+
the session's `meta.json` (atomic write).
|
|
16
|
+
|
|
17
|
+
The mixin holds no state of its own — every method uses attributes set by
|
|
18
|
+
`ShadowGit.__init__` (`self.config`, `self.project_path`) and helpers from
|
|
19
|
+
the core class (`self._run`).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import tempfile
|
|
27
|
+
import time
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from painapple_code.paths import get_sessions_dir
|
|
31
|
+
from painapple_code.subprocess_registry import agent_subprocesses, SubprocessType, SubprocessStatus
|
|
32
|
+
from painapple_code.utils.proc import popen_kwargs_detached, resolve_binary
|
|
33
|
+
from painapple_code.turn_tracker import TurnTracker
|
|
34
|
+
|
|
35
|
+
from painapple_code.shadow_git_frontmatter import (
|
|
36
|
+
SummaryCost,
|
|
37
|
+
MAX_JOURNEY_TURNS,
|
|
38
|
+
build_journey_section,
|
|
39
|
+
parse_yaml_frontmatter,
|
|
40
|
+
)
|
|
41
|
+
from painapple_code.shadow_git_sections import (
|
|
42
|
+
build_commit_prompt_for_json,
|
|
43
|
+
build_commit_schema,
|
|
44
|
+
structured_to_markdown,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("painapple-code.shadow-git")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _SummaryMixin:
|
|
51
|
+
"""Rich-commit methods mixed into ShadowGit. Not instantiated directly."""
|
|
52
|
+
|
|
53
|
+
def _extract_title(self, message_body: str) -> str:
|
|
54
|
+
"""Extract title from rich commit message (first ## Summary line)."""
|
|
55
|
+
for line in message_body.split("\n"):
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if line.startswith("## Summary"):
|
|
58
|
+
continue
|
|
59
|
+
if line and not line.startswith("#"):
|
|
60
|
+
return line[:60]
|
|
61
|
+
return "Update"
|
|
62
|
+
|
|
63
|
+
async def _build_journey_context(
|
|
64
|
+
self,
|
|
65
|
+
session_id: str,
|
|
66
|
+
current_branch: Optional[str] = None
|
|
67
|
+
) -> list[dict]:
|
|
68
|
+
"""
|
|
69
|
+
Build journey context from previous commits for this session.
|
|
70
|
+
|
|
71
|
+
Fetches previous commits and extracts their summaries from YAML frontmatter.
|
|
72
|
+
When current_branch is provided, only includes entries from the same branch
|
|
73
|
+
to prevent context bleeding across branches.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
session_id: Session to get journey for
|
|
77
|
+
current_branch: Current project branch (for filtering)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of journey entries: [{turn: 1, summary: "..."}, ...]
|
|
81
|
+
"""
|
|
82
|
+
journey = []
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Get previous commits for this session (most recent first)
|
|
86
|
+
# Fetch extra commits to allow for branch filtering
|
|
87
|
+
stdout, _, exit_code = await self._run([
|
|
88
|
+
"log",
|
|
89
|
+
f"--grep=\\[{session_id[:8]}",
|
|
90
|
+
f"-n{MAX_JOURNEY_TURNS * 2 + 1}", # Get extra for branch filtering
|
|
91
|
+
"--format=%H"
|
|
92
|
+
], check=False)
|
|
93
|
+
|
|
94
|
+
if exit_code != 0 or not stdout.strip():
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
commit_hashes = stdout.strip().split("\n")
|
|
98
|
+
|
|
99
|
+
# Fetch full commit message for each and extract summary
|
|
100
|
+
# Get extra commits to allow for branch filtering
|
|
101
|
+
for commit_hash in commit_hashes[:MAX_JOURNEY_TURNS * 2]:
|
|
102
|
+
if not commit_hash:
|
|
103
|
+
continue
|
|
104
|
+
if len(journey) >= MAX_JOURNEY_TURNS:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
msg_stdout, _, _ = await self._run([
|
|
109
|
+
"log", "-1", "--format=%B", commit_hash
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
# Parse YAML frontmatter
|
|
113
|
+
yaml_data = parse_yaml_frontmatter(msg_stdout)
|
|
114
|
+
|
|
115
|
+
if yaml_data:
|
|
116
|
+
turn = yaml_data.get('turn', 0)
|
|
117
|
+
summary = yaml_data.get('summary', '')
|
|
118
|
+
commit_branch = yaml_data.get('project_branch')
|
|
119
|
+
|
|
120
|
+
# Filter by branch: skip entries from different branches
|
|
121
|
+
# Include if: no current_branch, no commit_branch (old format), or branches match
|
|
122
|
+
if current_branch and commit_branch and commit_branch != current_branch:
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"Skipping journey entry from different branch: "
|
|
125
|
+
f"{commit_branch} != {current_branch}"
|
|
126
|
+
)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if turn and summary:
|
|
130
|
+
journey.append({
|
|
131
|
+
'turn': turn,
|
|
132
|
+
'summary': summary
|
|
133
|
+
})
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.debug(f"Failed to parse commit {commit_hash[:8]}: {e}")
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Sort by turn number (oldest first for chronological order)
|
|
139
|
+
journey.sort(key=lambda x: x.get('turn', 0))
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Failed to build journey context: {e}")
|
|
143
|
+
|
|
144
|
+
return journey
|
|
145
|
+
|
|
146
|
+
async def _generate_rich_commit_message(
|
|
147
|
+
self,
|
|
148
|
+
provider,
|
|
149
|
+
provider_session_id: str,
|
|
150
|
+
prompt: str,
|
|
151
|
+
tracker: TurnTracker,
|
|
152
|
+
journey: list[dict] = None,
|
|
153
|
+
app_session_id: Optional[str] = None,
|
|
154
|
+
token_profile: Optional[str] = None,
|
|
155
|
+
session_model: Optional[str] = None,
|
|
156
|
+
) -> tuple[Optional[str], Optional[SummaryCost], Optional[dict]]:
|
|
157
|
+
"""
|
|
158
|
+
Fork the session's provider to generate a structured commit message.
|
|
159
|
+
|
|
160
|
+
The provider owns the fork mechanism and result parsing
|
|
161
|
+
(`build_summary_fork` / `parse_summary_fork`): Claude branches with
|
|
162
|
+
`--resume --fork-session`, Codex copies its rollout under a fresh id and
|
|
163
|
+
resumes the copy. This method owns the provider-agnostic plumbing —
|
|
164
|
+
prompt + schema assembly, the subprocess registry entry, the 300s
|
|
165
|
+
timeout, cancellation, temp-file cleanup, and rendering the structured
|
|
166
|
+
result to markdown.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
provider: The session's Provider (supplies the fork argv + parser).
|
|
170
|
+
provider_session_id: Provider session/thread id the fork resumes.
|
|
171
|
+
prompt: User's prompt for this turn
|
|
172
|
+
tracker: TurnTracker with files and tools
|
|
173
|
+
journey: Previous turns' summaries for context
|
|
174
|
+
app_session_id: painapple-code session ID (for subprocess tracking)
|
|
175
|
+
token_profile: OAuth token profile for the fork (Claude only)
|
|
176
|
+
session_model: Parent session's model ID (to inherit context tier)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Tuple of (message_body, summary_cost, structured_data), or
|
|
180
|
+
(None, None, None) when the provider can't fork or the fork fails.
|
|
181
|
+
"""
|
|
182
|
+
if not provider_session_id:
|
|
183
|
+
return None, None, None
|
|
184
|
+
|
|
185
|
+
# Build journey section for the prompt
|
|
186
|
+
journey_section = build_journey_section(journey or [])
|
|
187
|
+
|
|
188
|
+
# Build prompt and schema for structured JSON output
|
|
189
|
+
prompt_type = "file_changes" if tracker.has_file_changes else "tool_only"
|
|
190
|
+
fork_prompt = build_commit_prompt_for_json(
|
|
191
|
+
project_config=self.config,
|
|
192
|
+
prompt_type=prompt_type,
|
|
193
|
+
journey_section=journey_section,
|
|
194
|
+
user_prompt=prompt[:500],
|
|
195
|
+
files=", ".join(sorted(tracker.modified_files)) if tracker.has_file_changes else "",
|
|
196
|
+
tools=tracker.format_for_prompt()
|
|
197
|
+
)
|
|
198
|
+
schema_str = json.dumps(build_commit_schema(self.config, prompt_type))
|
|
199
|
+
|
|
200
|
+
# Ask the provider how to fork itself for this turn. None → the provider
|
|
201
|
+
# can't fork (unsupported, or its rollout is missing); the basic shadow
|
|
202
|
+
# commit still happens upstream, just without AI sections.
|
|
203
|
+
plan = provider.build_summary_fork(
|
|
204
|
+
session_id=provider_session_id,
|
|
205
|
+
fork_prompt=fork_prompt,
|
|
206
|
+
schema_json=schema_str,
|
|
207
|
+
cwd=str(self.project_path),
|
|
208
|
+
token_profile=token_profile,
|
|
209
|
+
session_model=session_model,
|
|
210
|
+
)
|
|
211
|
+
if plan is None:
|
|
212
|
+
return None, None, None
|
|
213
|
+
|
|
214
|
+
proc = None
|
|
215
|
+
# A provider whose wire isn't the default newline-JSON ("lines") conducts
|
|
216
|
+
# the summary over the live process itself (drive_summary_fork) instead of
|
|
217
|
+
# a one-shot communicate() + parse_summary_fork — e.g. the app-server's
|
|
218
|
+
# JSON-RPC thread/fork + turn/start. We still own spawn/registry/timeout.
|
|
219
|
+
transport_driven = getattr(provider.capabilities, "transport", "lines") != "lines"
|
|
220
|
+
# A provider may hand us the prompt to write over stdin (keeps a large
|
|
221
|
+
# prompt out of argv/ps and clear of ARG_MAX) — that also needs a PIPE.
|
|
222
|
+
stdin_input = getattr(plan, "stdin_input", None)
|
|
223
|
+
try:
|
|
224
|
+
start_time = time.time()
|
|
225
|
+
stdout, stderr = b"", b""
|
|
226
|
+
|
|
227
|
+
proc = await asyncio.create_subprocess_exec(
|
|
228
|
+
resolve_binary(plan.argv[0]), *plan.argv[1:],
|
|
229
|
+
stdin=asyncio.subprocess.PIPE if (transport_driven or stdin_input is not None) else None,
|
|
230
|
+
stdout=asyncio.subprocess.PIPE,
|
|
231
|
+
stderr=asyncio.subprocess.PIPE,
|
|
232
|
+
cwd=str(self.project_path),
|
|
233
|
+
env=plan.env,
|
|
234
|
+
**popen_kwargs_detached(), # isolate from server's process group
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Register subprocess for visibility in Active Sessions widget
|
|
238
|
+
agent_subprocesses.register(
|
|
239
|
+
pid=proc.pid,
|
|
240
|
+
subprocess_type=SubprocessType.SUMMARY_FORK,
|
|
241
|
+
parent_session_id=app_session_id,
|
|
242
|
+
model=plan.model,
|
|
243
|
+
purpose="Rich commit message",
|
|
244
|
+
cwd=str(self.project_path),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if transport_driven:
|
|
248
|
+
structured_data, cost = await asyncio.wait_for(
|
|
249
|
+
provider.drive_summary_fork(proc, plan),
|
|
250
|
+
timeout=300, # summarizer may need compaction
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
stdout, stderr = await asyncio.wait_for(
|
|
254
|
+
proc.communicate(
|
|
255
|
+
input=stdin_input.encode() if stdin_input is not None else None
|
|
256
|
+
),
|
|
257
|
+
timeout=300 # summarizer may need compaction when context exceeds the window
|
|
258
|
+
)
|
|
259
|
+
# Provider parses its own output → (structured_data, cost).
|
|
260
|
+
structured_data, cost = provider.parse_summary_fork(
|
|
261
|
+
plan=plan,
|
|
262
|
+
returncode=proc.returncode or 0,
|
|
263
|
+
stdout=stdout or b"",
|
|
264
|
+
stderr=stderr or b"",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
duration = time.time() - start_time
|
|
268
|
+
|
|
269
|
+
if not structured_data:
|
|
270
|
+
err = stderr.decode(errors="replace").strip() if stderr else ""
|
|
271
|
+
if not err and stdout:
|
|
272
|
+
err = stdout.decode(errors="replace").strip()[:300]
|
|
273
|
+
logger.warning(
|
|
274
|
+
f"Rich-commit fork failed (rc={proc.returncode}): "
|
|
275
|
+
f"{err[:300] or 'no structured output'}"
|
|
276
|
+
)
|
|
277
|
+
agent_subprocesses.unregister(
|
|
278
|
+
proc.pid,
|
|
279
|
+
status=SubprocessStatus.ERROR,
|
|
280
|
+
error_message=err[:200] or "no structured output",
|
|
281
|
+
)
|
|
282
|
+
return None, None, None
|
|
283
|
+
|
|
284
|
+
summary_cost = SummaryCost(
|
|
285
|
+
cost=(cost or {}).get("cost", 0.0),
|
|
286
|
+
input_tokens=(cost or {}).get("input_tokens", 0),
|
|
287
|
+
output_tokens=(cost or {}).get("output_tokens", 0),
|
|
288
|
+
duration=duration,
|
|
289
|
+
)
|
|
290
|
+
message = structured_to_markdown(structured_data, self.config, prompt_type)
|
|
291
|
+
|
|
292
|
+
logger.info(
|
|
293
|
+
f"Rich-commit fork: ${summary_cost.cost:.4f} "
|
|
294
|
+
f"({summary_cost.input_tokens}↓ {summary_cost.output_tokens}↑) "
|
|
295
|
+
f"in {duration:.1f}s"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Unregister with success status and full details
|
|
299
|
+
agent_subprocesses.unregister(
|
|
300
|
+
proc.pid,
|
|
301
|
+
status=SubprocessStatus.SUCCESS,
|
|
302
|
+
result_preview=message[:100] if message else None,
|
|
303
|
+
cost=summary_cost.cost,
|
|
304
|
+
input_tokens=summary_cost.input_tokens,
|
|
305
|
+
output_tokens=summary_cost.output_tokens,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return message, summary_cost, structured_data
|
|
309
|
+
|
|
310
|
+
except asyncio.TimeoutError:
|
|
311
|
+
logger.warning("Rich-commit fork timed out after 300s")
|
|
312
|
+
# Unregister with timeout status
|
|
313
|
+
if proc:
|
|
314
|
+
agent_subprocesses.unregister(
|
|
315
|
+
proc.pid,
|
|
316
|
+
status=SubprocessStatus.TIMEOUT,
|
|
317
|
+
error_message="Process timed out after 300s",
|
|
318
|
+
)
|
|
319
|
+
# Kill the process if still running
|
|
320
|
+
if proc.returncode is None:
|
|
321
|
+
try:
|
|
322
|
+
proc.kill()
|
|
323
|
+
await proc.wait()
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
return None, None, None
|
|
327
|
+
except asyncio.CancelledError:
|
|
328
|
+
logger.debug("Rich-commit fork cancelled (user sent new prompt)")
|
|
329
|
+
# Unregister with killed status
|
|
330
|
+
if proc:
|
|
331
|
+
agent_subprocesses.unregister(
|
|
332
|
+
proc.pid,
|
|
333
|
+
status=SubprocessStatus.KILLED,
|
|
334
|
+
error_message="Cancelled by new user prompt",
|
|
335
|
+
)
|
|
336
|
+
# Kill the process if still running
|
|
337
|
+
if proc.returncode is None:
|
|
338
|
+
try:
|
|
339
|
+
proc.kill()
|
|
340
|
+
await proc.wait()
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
raise # Re-raise to propagate cancellation
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.exception(f"Rich-commit fork error: {e}")
|
|
346
|
+
# Unregister with error status
|
|
347
|
+
if proc:
|
|
348
|
+
agent_subprocesses.unregister(
|
|
349
|
+
proc.pid,
|
|
350
|
+
status=SubprocessStatus.ERROR,
|
|
351
|
+
error_message=str(e)[:200],
|
|
352
|
+
)
|
|
353
|
+
return None, None, None
|
|
354
|
+
finally:
|
|
355
|
+
# Reap a still-running process. The exec path exits via communicate()
|
|
356
|
+
# and the timeout/cancel branches kill explicitly; this backstops the
|
|
357
|
+
# transport-driven success path and a generic error before any kill.
|
|
358
|
+
if proc is not None and proc.returncode is None:
|
|
359
|
+
try:
|
|
360
|
+
proc.kill()
|
|
361
|
+
await proc.wait()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
# Remove any temp copies/files the provider created for this fork
|
|
365
|
+
# (Codex exec's rollout copy + schema/output temp files; none for
|
|
366
|
+
# Claude or the app-server's native fork).
|
|
367
|
+
for path in plan.cleanup_paths:
|
|
368
|
+
try:
|
|
369
|
+
os.unlink(path)
|
|
370
|
+
except OSError:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
async def _update_session_meta(
|
|
374
|
+
self,
|
|
375
|
+
session_id: str,
|
|
376
|
+
commit_hash: str,
|
|
377
|
+
tracker: TurnTracker,
|
|
378
|
+
cost_info,
|
|
379
|
+
summary_cost: Optional[SummaryCost] = None,
|
|
380
|
+
session_title: Optional[str] = None
|
|
381
|
+
):
|
|
382
|
+
"""Update session's meta.json with shadow git info and optional title."""
|
|
383
|
+
sessions_dir = get_sessions_dir(str(self.project_path))
|
|
384
|
+
meta_file = sessions_dir / session_id / "meta.json"
|
|
385
|
+
|
|
386
|
+
if not meta_file.exists():
|
|
387
|
+
logger.warning(f"Session meta not found: {meta_file}")
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
392
|
+
|
|
393
|
+
# Update session title if provided (from the summary fork)
|
|
394
|
+
# Skip if user manually renamed the session (manual_name flag)
|
|
395
|
+
if session_title and not meta.get("manual_name"):
|
|
396
|
+
meta["name"] = session_title
|
|
397
|
+
logger.debug(f"Updated session title: {session_title}")
|
|
398
|
+
|
|
399
|
+
if "shadow" not in meta:
|
|
400
|
+
meta["shadow"] = {
|
|
401
|
+
"turn_count": 0,
|
|
402
|
+
"file_commits": 0,
|
|
403
|
+
"tool_commits": 0,
|
|
404
|
+
"first_commit": commit_hash,
|
|
405
|
+
"files_touched": [],
|
|
406
|
+
"tools_summary": {},
|
|
407
|
+
"total_cost": 0,
|
|
408
|
+
"total_input_tokens": 0,
|
|
409
|
+
"total_output_tokens": 0,
|
|
410
|
+
"summary_cost": 0,
|
|
411
|
+
"summary_calls": 0,
|
|
412
|
+
"archived": False,
|
|
413
|
+
"archive_tag": None
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
shadow = meta["shadow"]
|
|
417
|
+
shadow["turn_count"] += 1
|
|
418
|
+
shadow["last_commit"] = commit_hash
|
|
419
|
+
|
|
420
|
+
if tracker.has_file_changes:
|
|
421
|
+
shadow["file_commits"] += 1
|
|
422
|
+
shadow["files_touched"] = list(set(
|
|
423
|
+
shadow["files_touched"] + list(tracker.modified_files)
|
|
424
|
+
))
|
|
425
|
+
else:
|
|
426
|
+
shadow["tool_commits"] += 1
|
|
427
|
+
|
|
428
|
+
# Merge tool counts
|
|
429
|
+
for name, count in tracker.get_tools_summary().items():
|
|
430
|
+
shadow["tools_summary"][name] = shadow["tools_summary"].get(name, 0) + count
|
|
431
|
+
|
|
432
|
+
shadow["total_cost"] += cost_info.cost
|
|
433
|
+
shadow["total_input_tokens"] += cost_info.input_tokens
|
|
434
|
+
shadow["total_output_tokens"] += cost_info.output_tokens
|
|
435
|
+
|
|
436
|
+
# Track summary-fork costs for rich commits.
|
|
437
|
+
# Back-compat: migrate the pre-rename haiku_* keys on first write.
|
|
438
|
+
if summary_cost:
|
|
439
|
+
shadow["summary_cost"] = shadow.get("summary_cost", shadow.pop("haiku_cost", 0)) + summary_cost.cost
|
|
440
|
+
shadow["summary_calls"] = shadow.get("summary_calls", shadow.pop("haiku_calls", 0)) + 1
|
|
441
|
+
|
|
442
|
+
# Atomic write to avoid corruption on disk-full
|
|
443
|
+
tmp_path = None
|
|
444
|
+
data = json.dumps(meta, indent=2).encode()
|
|
445
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
446
|
+
dir=meta_file.parent, prefix=".meta_", suffix=".tmp"
|
|
447
|
+
)
|
|
448
|
+
try:
|
|
449
|
+
os.write(fd, data)
|
|
450
|
+
os.fsync(fd)
|
|
451
|
+
finally:
|
|
452
|
+
os.close(fd)
|
|
453
|
+
os.replace(tmp_path, meta_file)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.exception(f"Failed to update session meta: {e}")
|
|
457
|
+
# Clean up temp file if atomic write failed partway
|
|
458
|
+
if tmp_path:
|
|
459
|
+
try:
|
|
460
|
+
os.unlink(tmp_path)
|
|
461
|
+
except OSError:
|
|
462
|
+
pass
|