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,196 @@
|
|
|
1
|
+
"""Codex provider — the rich-commit summary fork.
|
|
2
|
+
|
|
3
|
+
`codex exec resume` continues a thread *in place*, so summarizing the turn by
|
|
4
|
+
resuming the user's real thread would pollute their conversation. This mixin
|
|
5
|
+
copies the thread's rollout under a fresh id and resumes the copy — the
|
|
6
|
+
equivalent of Claude's `--fork-session` — then reads the structured JSON back.
|
|
7
|
+
Mixed into `CodexProvider`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from painapple_code import paths
|
|
17
|
+
from painapple_code.providers.base import SummaryForkPlan
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _SummaryMixin:
|
|
21
|
+
"""Branch off the real thread and summarize the turn with Codex itself."""
|
|
22
|
+
|
|
23
|
+
def build_summary_fork(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
session_id: str,
|
|
27
|
+
fork_prompt: str,
|
|
28
|
+
schema_json: str,
|
|
29
|
+
cwd: str,
|
|
30
|
+
token_profile: Optional[str] = None,
|
|
31
|
+
session_model: Optional[str] = None,
|
|
32
|
+
) -> Optional[SummaryForkPlan]:
|
|
33
|
+
"""Summarize the turn with Codex itself, branching off the real thread.
|
|
34
|
+
|
|
35
|
+
`codex exec resume` continues a thread *in place* (and `--ephemeral`
|
|
36
|
+
doesn't prevent that on resume), so resuming the user's thread would
|
|
37
|
+
append every summary turn into their conversation. Instead we copy the
|
|
38
|
+
thread's rollout under a fresh id and resume the copy — the equivalent of
|
|
39
|
+
Claude's `--fork-session`. `--output-schema` forces the structured shape
|
|
40
|
+
(strict mode: every property required) and `-o` writes the final JSON to
|
|
41
|
+
a temp file. Returns None if the rollout can't be located, in which case
|
|
42
|
+
the turn still gets a basic shadow commit without AI sections.
|
|
43
|
+
"""
|
|
44
|
+
import tempfile
|
|
45
|
+
|
|
46
|
+
fork = self._fork_rollout(session_id)
|
|
47
|
+
if fork is None:
|
|
48
|
+
return None
|
|
49
|
+
fork_id, copy_path = fork
|
|
50
|
+
|
|
51
|
+
schema_fd, schema_path = tempfile.mkstemp(prefix="codex_schema_", suffix=".json")
|
|
52
|
+
with os.fdopen(schema_fd, "w", encoding="utf-8") as f:
|
|
53
|
+
f.write(self._strictify_schema(schema_json))
|
|
54
|
+
out_fd, out_path = tempfile.mkstemp(prefix="codex_summary_", suffix=".json")
|
|
55
|
+
os.close(out_fd)
|
|
56
|
+
|
|
57
|
+
# `resume` has no -s flag; sandbox is a config override. read-only keeps
|
|
58
|
+
# the summarizer from touching the workspace.
|
|
59
|
+
argv = [
|
|
60
|
+
self.binary(), "exec", "resume", fork_id,
|
|
61
|
+
"--json", "--skip-git-repo-check",
|
|
62
|
+
"-c", 'sandbox_mode="read-only"',
|
|
63
|
+
"--output-schema", schema_path,
|
|
64
|
+
"-o", out_path,
|
|
65
|
+
]
|
|
66
|
+
# Optional cheaper summarizer model (seam-described override);
|
|
67
|
+
# default inherits the thread's model.
|
|
68
|
+
summary_model = self.get_summary_model_override()
|
|
69
|
+
if summary_model and not summary_model.startswith("claude"):
|
|
70
|
+
argv += ["-m", summary_model]
|
|
71
|
+
argv.append(fork_prompt)
|
|
72
|
+
|
|
73
|
+
return SummaryForkPlan(
|
|
74
|
+
argv=argv,
|
|
75
|
+
env=None, # codex uses ambient auth (codex login / CODEX_API_KEY)
|
|
76
|
+
model=summary_model or "codex",
|
|
77
|
+
output_file=out_path,
|
|
78
|
+
cleanup_paths=[copy_path, schema_path, out_path],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def parse_summary_fork(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
plan: SummaryForkPlan,
|
|
85
|
+
returncode: int,
|
|
86
|
+
stdout: bytes,
|
|
87
|
+
stderr: bytes,
|
|
88
|
+
) -> tuple[Optional[dict], Optional[dict]]:
|
|
89
|
+
"""Read the structured JSON from `-o` (fallback: last agent_message)."""
|
|
90
|
+
if returncode != 0:
|
|
91
|
+
return None, None
|
|
92
|
+
structured = None
|
|
93
|
+
if plan.output_file:
|
|
94
|
+
try:
|
|
95
|
+
text = open(plan.output_file, encoding="utf-8").read().strip()
|
|
96
|
+
if text:
|
|
97
|
+
structured = json.loads(text)
|
|
98
|
+
except (OSError, json.JSONDecodeError):
|
|
99
|
+
structured = None
|
|
100
|
+
if not isinstance(structured, dict):
|
|
101
|
+
structured = self._structured_from_stream(stdout)
|
|
102
|
+
if not isinstance(structured, dict):
|
|
103
|
+
return None, None
|
|
104
|
+
usage = self._usage_from_stream(stdout)
|
|
105
|
+
cost = {
|
|
106
|
+
"cost": 0.0, # Codex reports no USD cost
|
|
107
|
+
"input_tokens": usage.get("input_tokens", 0) or 0,
|
|
108
|
+
"output_tokens": usage.get("output_tokens", 0) or 0,
|
|
109
|
+
}
|
|
110
|
+
return structured, cost
|
|
111
|
+
|
|
112
|
+
# --- summary-fork helpers ---------------------------------------------
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _codex_sessions_dir():
|
|
116
|
+
"""Codex rollout dir (`$CODEX_HOME/sessions` or `~/.codex/sessions`)."""
|
|
117
|
+
from pathlib import Path
|
|
118
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
119
|
+
base = Path(codex_home) if codex_home else Path.home() / ".codex"
|
|
120
|
+
d = base / "sessions"
|
|
121
|
+
return d if d.is_dir() else None
|
|
122
|
+
|
|
123
|
+
def _fork_rollout(self, session_id: str) -> Optional[tuple[str, str]]:
|
|
124
|
+
"""Copy a thread's rollout under a fresh id. Returns (new_id, copy_path).
|
|
125
|
+
|
|
126
|
+
Codex stores each thread as `sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl`
|
|
127
|
+
with the uuid in both the filename and the line-1 `session_meta.id`.
|
|
128
|
+
Rewriting that uuid makes a resume-by-id land on the copy, leaving the
|
|
129
|
+
original byte-for-byte untouched.
|
|
130
|
+
"""
|
|
131
|
+
import uuid
|
|
132
|
+
|
|
133
|
+
sessions_dir = self._codex_sessions_dir()
|
|
134
|
+
if sessions_dir is None:
|
|
135
|
+
return None
|
|
136
|
+
matches = sorted(sessions_dir.glob(f"**/*{session_id}*.jsonl"))
|
|
137
|
+
if not matches:
|
|
138
|
+
return None
|
|
139
|
+
src = matches[0]
|
|
140
|
+
new_id = str(uuid.uuid4())
|
|
141
|
+
dst = src.with_name(src.name.replace(session_id, new_id))
|
|
142
|
+
try:
|
|
143
|
+
dst.write_text(src.read_text(encoding="utf-8").replace(session_id, new_id),
|
|
144
|
+
encoding="utf-8")
|
|
145
|
+
except OSError:
|
|
146
|
+
return None
|
|
147
|
+
return new_id, str(dst)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _strictify_schema(schema_json: str) -> str:
|
|
151
|
+
"""Widen `required` to every property — Codex's `--output-schema` runs in
|
|
152
|
+
OpenAI strict mode. The model emits empties for inapplicable sections,
|
|
153
|
+
which `structured_to_markdown` skips."""
|
|
154
|
+
try:
|
|
155
|
+
schema = json.loads(schema_json)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
return schema_json
|
|
158
|
+
props = schema.get("properties")
|
|
159
|
+
if isinstance(props, dict):
|
|
160
|
+
schema["required"] = list(props.keys())
|
|
161
|
+
schema["additionalProperties"] = False
|
|
162
|
+
return json.dumps(schema)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def _iter_events(cls, stdout: bytes):
|
|
166
|
+
for line in stdout.decode(errors="replace").splitlines():
|
|
167
|
+
line = line.strip()
|
|
168
|
+
if not line:
|
|
169
|
+
continue
|
|
170
|
+
try:
|
|
171
|
+
yield json.loads(line)
|
|
172
|
+
except json.JSONDecodeError:
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def _structured_from_stream(cls, stdout: bytes) -> Optional[dict]:
|
|
177
|
+
"""Fallback when `-o` is empty: the last agent_message holds the JSON."""
|
|
178
|
+
last = None
|
|
179
|
+
for ev in cls._iter_events(stdout):
|
|
180
|
+
if ev.get("type") == "item.completed":
|
|
181
|
+
item = ev.get("item") or {}
|
|
182
|
+
if item.get("type") == "agent_message":
|
|
183
|
+
last = item.get("text")
|
|
184
|
+
if last:
|
|
185
|
+
try:
|
|
186
|
+
return json.loads(last)
|
|
187
|
+
except json.JSONDecodeError:
|
|
188
|
+
return None
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def _usage_from_stream(cls, stdout: bytes) -> dict:
|
|
193
|
+
for ev in cls._iter_events(stdout):
|
|
194
|
+
if ev.get("type") == "turn.completed":
|
|
195
|
+
return ev.get("usage") or {}
|
|
196
|
+
return {}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Codex provider — event translation into the canonical Claude-shaped stream.
|
|
2
|
+
|
|
3
|
+
`translate_events` maps Codex's native event schema into the canonical
|
|
4
|
+
Claude-shaped events (`system`/`assistant`/`user`/`result`) the session handlers
|
|
5
|
+
and the frontend already understand:
|
|
6
|
+
|
|
7
|
+
thread.started → system/init (+ session id via session_id_from_event)
|
|
8
|
+
turn.started → (nothing)
|
|
9
|
+
item.completed reasoning → assistant {thinking}
|
|
10
|
+
item.completed agent_message → assistant {text}
|
|
11
|
+
item.* command_execution → assistant {tool_use Bash} + user {tool_result}
|
|
12
|
+
item.completed file_change → assistant {tool_use Edit/Write} + user {tool_result}
|
|
13
|
+
item.completed web_search → assistant {tool_use WebSearch} + user {tool_result}
|
|
14
|
+
item.* mcp_tool_call → assistant {tool_use mcp__…} + user {tool_result}
|
|
15
|
+
item.completed todo_list → assistant {tool_use TodoWrite} + user {tool_result}
|
|
16
|
+
turn.completed → result (tokens only; no USD cost)
|
|
17
|
+
turn.failed / error → result (is_error=True)
|
|
18
|
+
|
|
19
|
+
Codex reports token counts but no per-turn USD cost, so result cost is left at 0.
|
|
20
|
+
Token usage is per-turn already, so no cumulative→delta conversion is needed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _TranslateMixin:
|
|
30
|
+
"""Codex native events → canonical Claude-shaped events."""
|
|
31
|
+
|
|
32
|
+
def session_id_from_event(self, event: dict) -> Optional[str]:
|
|
33
|
+
if event.get("type") == "thread.started":
|
|
34
|
+
return event.get("thread_id")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
def translate_events(self, event: dict, state: dict) -> list[dict]:
|
|
38
|
+
etype = event.get("type")
|
|
39
|
+
|
|
40
|
+
if etype == "thread.started":
|
|
41
|
+
init = {"type": "system", "subtype": "init",
|
|
42
|
+
"session_id": event.get("thread_id")}
|
|
43
|
+
model = state.get("model")
|
|
44
|
+
if model and not model.startswith("claude"):
|
|
45
|
+
init["model"] = model
|
|
46
|
+
return [init]
|
|
47
|
+
|
|
48
|
+
if etype == "turn.started":
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
if etype == "turn.completed":
|
|
52
|
+
return [self._result_from_usage(event.get("usage") or {}, state)]
|
|
53
|
+
|
|
54
|
+
if etype in ("turn.failed", "error"):
|
|
55
|
+
err = event.get("error") or {}
|
|
56
|
+
message = err.get("message") if isinstance(err, dict) else None
|
|
57
|
+
message = message or event.get("message") or "Codex turn failed"
|
|
58
|
+
return [self._error_result(message, state)]
|
|
59
|
+
|
|
60
|
+
if etype in ("item.started", "item.updated", "item.completed"):
|
|
61
|
+
return self._translate_item(etype, event.get("item") or {}, state)
|
|
62
|
+
|
|
63
|
+
# Unknown event — ignore rather than surfacing noise.
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
# --- item translation -------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _translate_item(self, etype: str, item: dict, state: dict) -> list[dict]:
|
|
69
|
+
itype = item.get("type")
|
|
70
|
+
item_id = item.get("id") or ""
|
|
71
|
+
completed = etype == "item.completed"
|
|
72
|
+
started = etype == "item.started"
|
|
73
|
+
|
|
74
|
+
if itype == "reasoning" and completed:
|
|
75
|
+
return [self._assistant([{"type": "thinking",
|
|
76
|
+
"thinking": item.get("text", "")}])]
|
|
77
|
+
|
|
78
|
+
if itype == "agent_message" and completed:
|
|
79
|
+
text = item.get("text", "")
|
|
80
|
+
state["last_text"] = text # surfaced as result.result
|
|
81
|
+
return [self._assistant([{"type": "text", "text": text}])]
|
|
82
|
+
|
|
83
|
+
if itype == "command_execution":
|
|
84
|
+
if started:
|
|
85
|
+
return [self._tool_use(item_id, "Bash",
|
|
86
|
+
{"command": item.get("command", "")})]
|
|
87
|
+
if completed:
|
|
88
|
+
out = item.get("aggregated_output", "")
|
|
89
|
+
exit_code = item.get("exit_code")
|
|
90
|
+
if exit_code not in (None, 0):
|
|
91
|
+
out = f"{out}\n[exit code: {exit_code}]"
|
|
92
|
+
return [self._tool_result(item_id, out)]
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
if itype == "file_change" and completed:
|
|
96
|
+
msgs: list[dict] = []
|
|
97
|
+
for i, change in enumerate(item.get("changes") or []):
|
|
98
|
+
path = change.get("path", "")
|
|
99
|
+
kind = change.get("kind", "update")
|
|
100
|
+
tool = "Write" if kind == "add" else "Edit"
|
|
101
|
+
cid = f"{item_id}_{i}"
|
|
102
|
+
msgs.append(self._tool_use(cid, tool, {"file_path": path}))
|
|
103
|
+
msgs.append(self._tool_result(cid, f"{kind}: {path}"))
|
|
104
|
+
return msgs
|
|
105
|
+
|
|
106
|
+
if itype == "web_search" and completed:
|
|
107
|
+
return [
|
|
108
|
+
self._tool_use(item_id, "WebSearch", {"query": item.get("query", "")}),
|
|
109
|
+
self._tool_result(item_id, "Search completed."),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
if itype == "mcp_tool_call":
|
|
113
|
+
name = f"mcp__{item.get('server', '')}__{item.get('tool', '')}"
|
|
114
|
+
if started:
|
|
115
|
+
return [self._tool_use(item_id, name, item.get("arguments") or {})]
|
|
116
|
+
if completed:
|
|
117
|
+
result = item.get("result")
|
|
118
|
+
err = item.get("error")
|
|
119
|
+
content = err if err else json.dumps(result) if result is not None else "(no result)"
|
|
120
|
+
return [self._tool_result(item_id, str(content))]
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
if itype == "todo_list" and completed:
|
|
124
|
+
todos = [
|
|
125
|
+
{"content": t.get("text", ""),
|
|
126
|
+
"status": "completed" if t.get("completed") else "pending",
|
|
127
|
+
"activeForm": t.get("text", "")}
|
|
128
|
+
for t in (item.get("items") or [])
|
|
129
|
+
]
|
|
130
|
+
return [
|
|
131
|
+
self._tool_use(item_id, "TodoWrite", {"todos": todos}),
|
|
132
|
+
self._tool_result(item_id, "Todos updated."),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if itype == "error" and completed:
|
|
136
|
+
return [self._assistant([{"type": "text",
|
|
137
|
+
"text": f"⚠️ {item.get('message', '')}"}])]
|
|
138
|
+
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
# --- canonical-event builders ----------------------------------------
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _assistant(blocks: list[dict]) -> dict:
|
|
145
|
+
return {"type": "assistant",
|
|
146
|
+
"message": {"role": "assistant", "content": blocks}}
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _tool_use(tool_id: str, name: str, tool_input: dict) -> dict:
|
|
150
|
+
return {"type": "assistant", "message": {"role": "assistant", "content": [
|
|
151
|
+
{"type": "tool_use", "id": tool_id, "name": name, "input": tool_input},
|
|
152
|
+
]}}
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _tool_result(tool_id: str, content: str) -> dict:
|
|
156
|
+
return {"type": "user", "message": {"role": "user", "content": [
|
|
157
|
+
{"type": "tool_result", "tool_use_id": tool_id, "content": content},
|
|
158
|
+
]}}
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _result_from_usage(usage: dict, state: dict) -> dict:
|
|
162
|
+
in_tok = usage.get("input_tokens", 0) or 0
|
|
163
|
+
out_tok = usage.get("output_tokens", 0) or 0
|
|
164
|
+
cache_read = usage.get("cached_input_tokens", 0) or 0
|
|
165
|
+
result = {
|
|
166
|
+
"type": "result",
|
|
167
|
+
"subtype": "success",
|
|
168
|
+
"is_error": False,
|
|
169
|
+
"total_cost_usd": 0, # Codex reports no USD cost
|
|
170
|
+
"duration_ms": 0, # finalize_turn derives this from turn_start
|
|
171
|
+
"num_turns": 1,
|
|
172
|
+
"result": state.get("last_text", ""),
|
|
173
|
+
"usage": {
|
|
174
|
+
"input_tokens": in_tok,
|
|
175
|
+
"output_tokens": out_tok,
|
|
176
|
+
"cache_read_input_tokens": cache_read,
|
|
177
|
+
"cache_creation_input_tokens": 0,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
# Only record a model breakdown under an OpenAI-shaped id — the seeded
|
|
181
|
+
# state model may be the Claude default that build_command dropped.
|
|
182
|
+
model = state.get("model")
|
|
183
|
+
if model and not model.startswith("claude"):
|
|
184
|
+
result["modelUsage"] = {model: {
|
|
185
|
+
"costUSD": 0,
|
|
186
|
+
"inputTokens": in_tok,
|
|
187
|
+
"outputTokens": out_tok,
|
|
188
|
+
"cacheReadInputTokens": cache_read,
|
|
189
|
+
"cacheCreationInputTokens": 0,
|
|
190
|
+
}}
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _error_result(message: str, state: dict) -> dict:
|
|
195
|
+
return {
|
|
196
|
+
"type": "result",
|
|
197
|
+
"subtype": "error_during_execution",
|
|
198
|
+
"is_error": True,
|
|
199
|
+
"total_cost_usd": 0,
|
|
200
|
+
"duration_ms": 0,
|
|
201
|
+
"num_turns": 1,
|
|
202
|
+
"result": message,
|
|
203
|
+
"usage": {},
|
|
204
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI Codex provider over the **app-server** protocol (`codex app-server`).
|
|
3
|
+
|
|
4
|
+
The sibling `codex` provider drives `codex exec --json` — one throwaway process
|
|
5
|
+
*per turn*, prompt in argv, newline-JSON to stdout, exit. That's the
|
|
6
|
+
lowest-common-denominator path; it's why that provider declares `fork=False`,
|
|
7
|
+
`context_command=False`, no real model list, and a hardcoded context window.
|
|
8
|
+
|
|
9
|
+
This provider instead speaks the **persistent JSON-RPC protocol** the official
|
|
10
|
+
Codex IDE extension uses (`codex app-server --stdio`): one long-lived process
|
|
11
|
+
serves many threads and turns, streaming events as JSON-RPC notifications. It
|
|
12
|
+
unlocks native fork, a real (server-reported) context window, token deltas, and
|
|
13
|
+
— later — interactive approvals, none of which `codex exec` can express. It is
|
|
14
|
+
marked experimental in the CLI, so it ships *alongside* the stable `codex`
|
|
15
|
+
provider rather than replacing it; users pick which they want.
|
|
16
|
+
|
|
17
|
+
Because the wire is JSON-RPC rather than one-way lines, this provider sets
|
|
18
|
+
`transport="jsonrpc"`: the session layer hands the spawned process to a
|
|
19
|
+
transport driver (see `transport.py`) that owns the bidirectional conversation —
|
|
20
|
+
the `initialize` handshake, lazy `thread/start`/`thread/resume`, `turn/start`,
|
|
21
|
+
id-correlated responses, and server-initiated approval requests. The read side
|
|
22
|
+
is unchanged: each notification still flows through `parse_line` +
|
|
23
|
+
`translate_events`.
|
|
24
|
+
|
|
25
|
+
Assembled from focused mixins (this file is the combiner — see `codex/` and
|
|
26
|
+
`claude/` for the same layout):
|
|
27
|
+
|
|
28
|
+
capabilities.py → _CapabilitiesMixin binary/auth, effort/perm vocab,
|
|
29
|
+
skill/agent/plugin roots, real
|
|
30
|
+
context window from token usage
|
|
31
|
+
launch.py → _LaunchMixin build_command (`codex app-server`)
|
|
32
|
+
transport.py → JsonRpcTransport the bidirectional JSON-RPC driver
|
|
33
|
+
translate.py → _TranslateMixin JSON-RPC notifications → Claude shape
|
|
34
|
+
summary.py → _SummaryMixin native rich-commit fork
|
|
35
|
+
(ephemeral thread/fork + outputSchema)
|
|
36
|
+
errors.py → _ErrorsMixin stderr classification / retry policy
|
|
37
|
+
|
|
38
|
+
The rich-commit summary is native here — an *ephemeral* `thread/fork` plus a
|
|
39
|
+
`turn/start` with `outputSchema`, no rollout-file copy (see `summary.py`). Only
|
|
40
|
+
stderr classification is reused verbatim from the `codex` exec provider (the
|
|
41
|
+
binary is the same, so its diagnostics are identical).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from painapple_code.providers.base import Provider, Capabilities
|
|
47
|
+
from painapple_code.providers.codex_app_server.capabilities import _CapabilitiesMixin
|
|
48
|
+
from painapple_code.providers.codex_app_server.launch import _LaunchMixin
|
|
49
|
+
from painapple_code.providers.codex_app_server.translate import _TranslateMixin
|
|
50
|
+
# Native app-server summary (ephemeral thread/fork + outputSchema; no rollout
|
|
51
|
+
# copy). Errors are reused verbatim from the exec provider — identical stderr.
|
|
52
|
+
from painapple_code.providers.codex_app_server.summary import _SummaryMixin
|
|
53
|
+
from painapple_code.providers.codex.errors import _ErrorsMixin
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CodexAppServerProvider(
|
|
57
|
+
_CapabilitiesMixin,
|
|
58
|
+
_LaunchMixin,
|
|
59
|
+
_TranslateMixin,
|
|
60
|
+
_SummaryMixin,
|
|
61
|
+
_ErrorsMixin,
|
|
62
|
+
Provider,
|
|
63
|
+
):
|
|
64
|
+
"""Adapter for the OpenAI Codex CLI over the app-server JSON-RPC protocol.
|
|
65
|
+
|
|
66
|
+
The behaviour lives in the mixins + the transport driver; this class carries
|
|
67
|
+
the static identity + capability flags and the MRO that composes them (mixins
|
|
68
|
+
first, so their overrides win, with `super()` calls — e.g. `is_available` —
|
|
69
|
+
still reaching the base implementation).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name = "codex-app-server"
|
|
73
|
+
display_name = "Codex (app-server)"
|
|
74
|
+
description = "OpenAI Codex app-server — persistent JSON-RPC, native fork"
|
|
75
|
+
capabilities = Capabilities(
|
|
76
|
+
resume=True,
|
|
77
|
+
fork=True, # native thread/fork (transport branches the
|
|
78
|
+
# source thread; no rollout copy)
|
|
79
|
+
permission_modes=True,
|
|
80
|
+
effort=True,
|
|
81
|
+
thinking_display=True, # native reasoning items
|
|
82
|
+
context_command=False, # metered from thread/tokenUsage, not a probe
|
|
83
|
+
cumulative_cost=False, # per-turn usage, no USD figure
|
|
84
|
+
rich_commit_summaries=True,
|
|
85
|
+
persistent_process=True, # one app-server process, many turns
|
|
86
|
+
forward_plain_stderr=False, # app-server logs human progress to stderr
|
|
87
|
+
transport="jsonrpc", # the session layer drives a transport driver
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def make_transport(self, process, opts, session):
|
|
91
|
+
# Imported lazily so the package import stays cheap and asyncio stays out
|
|
92
|
+
# of the static provider description.
|
|
93
|
+
from painapple_code.providers.codex_app_server.transport import JsonRpcTransport
|
|
94
|
+
return JsonRpcTransport(process, opts, session, self)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Registry contribution — discovered exactly like a flat module
|
|
98
|
+
# (pkgutil.iter_modules lists subpackages too), so adding this provider needed
|
|
99
|
+
# no edit to providers/__init__.py.
|
|
100
|
+
PROVIDERS = [CodexAppServerProvider()]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Codex app-server provider — declarative surface.
|
|
2
|
+
|
|
3
|
+
Everything the app-server declares about itself — binary/auth, effort &
|
|
4
|
+
permission vocabularies, skill/agent/plugin roots — is identical to the `codex`
|
|
5
|
+
exec provider (same CLI, same `$CODEX_HOME` layout), so this inherits the exec
|
|
6
|
+
capability mixin wholesale and overrides only context metering: the app-server
|
|
7
|
+
reports the *real* model context window (via the tokenUsage notification, stashed
|
|
8
|
+
onto the result by translate.py), so we meter against that instead of exec's
|
|
9
|
+
hardcoded family default.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from painapple_code.providers.codex.capabilities import (
|
|
17
|
+
_CapabilitiesMixin as _CodexExecCapabilitiesMixin,
|
|
18
|
+
_DEFAULT_CONTEXT_WINDOW,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _CapabilitiesMixin(_CodexExecCapabilitiesMixin):
|
|
23
|
+
"""Reuses the exec provider's self-description; real context window."""
|
|
24
|
+
|
|
25
|
+
def context_from_result(self, result_msg: dict, model: Optional[str] = None) -> Optional[dict]:
|
|
26
|
+
# input_tokens is the full conversation context fed to the model this
|
|
27
|
+
# turn — the live window occupancy. Unlike exec, the app-server tells us
|
|
28
|
+
# the actual window (carried on usage.context_window by translate.py); we
|
|
29
|
+
# fall back to the exec family default only if it's somehow absent.
|
|
30
|
+
usage = (result_msg or {}).get("usage") or {}
|
|
31
|
+
in_tok = usage.get("input_tokens") or 0
|
|
32
|
+
if not in_tok:
|
|
33
|
+
return None
|
|
34
|
+
window = usage.get("context_window") or _DEFAULT_CONTEXT_WINDOW
|
|
35
|
+
return {
|
|
36
|
+
"contextTokens": in_tok,
|
|
37
|
+
"contextWindow": window,
|
|
38
|
+
"percentage": round(in_tok / window * 100, 1) if window else None,
|
|
39
|
+
"breakdown": None,
|
|
40
|
+
"memoryFiles": None,
|
|
41
|
+
}
|