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.
Files changed (436) hide show
  1. painapple_code/__init__.py +30 -0
  2. painapple_code/__main__.py +18 -0
  3. painapple_code/_version.py +24 -0
  4. painapple_code/auth_middleware.py +930 -0
  5. painapple_code/cli/__init__.py +204 -0
  6. painapple_code/cli/deploy/__init__.py +15 -0
  7. painapple_code/cli/deploy/claude_seed.py +41 -0
  8. painapple_code/cli/deploy/config.py +260 -0
  9. painapple_code/cli/deploy/container.py +715 -0
  10. painapple_code/cli/deploy/launch.py +163 -0
  11. painapple_code/cli/deploy/runtime.py +223 -0
  12. painapple_code/cli/lifecycle_cmd.py +443 -0
  13. painapple_code/cli/list_cmd.py +572 -0
  14. painapple_code/cli/manage_cmd.py +670 -0
  15. painapple_code/cli/netinfo.py +125 -0
  16. painapple_code/cli/profiles.py +390 -0
  17. painapple_code/cli/serve_args.py +163 -0
  18. painapple_code/cli/serve_config.py +256 -0
  19. painapple_code/cli/setup_cmd.py +664 -0
  20. painapple_code/cli/ui.py +438 -0
  21. painapple_code/cost_analytics.py +879 -0
  22. painapple_code/data/models.defaults.yaml +18 -0
  23. painapple_code/data/models.yaml +18 -0
  24. painapple_code/data/presets.defaults.json +32 -0
  25. painapple_code/data/strings.yaml +2269 -0
  26. painapple_code/helpers.py +406 -0
  27. painapple_code/paths.py +1514 -0
  28. painapple_code/prompt_explorer.py +984 -0
  29. painapple_code/providers/__init__.py +224 -0
  30. painapple_code/providers/base.py +710 -0
  31. painapple_code/providers/claude/__init__.py +74 -0
  32. painapple_code/providers/claude/capabilities.py +218 -0
  33. painapple_code/providers/claude/errors.py +79 -0
  34. painapple_code/providers/claude/launch.py +50 -0
  35. painapple_code/providers/claude/summary.py +89 -0
  36. painapple_code/providers/claude/translate.py +74 -0
  37. painapple_code/providers/claude_sdk/__init__.py +139 -0
  38. painapple_code/providers/claude_sdk/driver.py +412 -0
  39. painapple_code/providers/codex/__init__.py +87 -0
  40. painapple_code/providers/codex/capabilities.py +321 -0
  41. painapple_code/providers/codex/errors.py +70 -0
  42. painapple_code/providers/codex/launch.py +142 -0
  43. painapple_code/providers/codex/summary.py +196 -0
  44. painapple_code/providers/codex/translate.py +204 -0
  45. painapple_code/providers/codex_app_server/__init__.py +100 -0
  46. painapple_code/providers/codex_app_server/capabilities.py +41 -0
  47. painapple_code/providers/codex_app_server/launch.py +202 -0
  48. painapple_code/providers/codex_app_server/summary.py +291 -0
  49. painapple_code/providers/codex_app_server/translate.py +263 -0
  50. painapple_code/providers/codex_app_server/transport.py +238 -0
  51. painapple_code/routes/__init__.py +0 -0
  52. painapple_code/routes/api_agents.py +441 -0
  53. painapple_code/routes/api_app.py +368 -0
  54. painapple_code/routes/api_app_commit_sections.py +127 -0
  55. painapple_code/routes/api_app_config.py +997 -0
  56. painapple_code/routes/api_app_session_prefs.py +217 -0
  57. painapple_code/routes/api_browser.py +690 -0
  58. painapple_code/routes/api_commands.py +374 -0
  59. painapple_code/routes/api_costs.py +128 -0
  60. painapple_code/routes/api_drafts.py +139 -0
  61. painapple_code/routes/api_exec.py +149 -0
  62. painapple_code/routes/api_files.py +542 -0
  63. painapple_code/routes/api_git.py +657 -0
  64. painapple_code/routes/api_logs.py +649 -0
  65. painapple_code/routes/api_plugins.py +275 -0
  66. painapple_code/routes/api_project_config.py +418 -0
  67. painapple_code/routes/api_prompts.py +231 -0
  68. painapple_code/routes/api_providers.py +53 -0
  69. painapple_code/routes/api_search.py +411 -0
  70. painapple_code/routes/api_session_stash.py +234 -0
  71. painapple_code/routes/api_session_welcome.py +151 -0
  72. painapple_code/routes/api_sessions.py +742 -0
  73. painapple_code/routes/api_shadow.py +323 -0
  74. painapple_code/routes/api_shadow_db.py +512 -0
  75. painapple_code/routes/api_shadow_files.py +332 -0
  76. painapple_code/routes/api_shadow_search.py +476 -0
  77. painapple_code/routes/api_skills.py +610 -0
  78. painapple_code/routes/api_tasks.py +147 -0
  79. painapple_code/routes/api_terminal.py +540 -0
  80. painapple_code/routes/api_upload.py +293 -0
  81. painapple_code/routes/api_viewer.py +229 -0
  82. painapple_code/routes/dependencies.py +109 -0
  83. painapple_code/routes/ws_chat.py +837 -0
  84. painapple_code/server.py +2208 -0
  85. painapple_code/server_logging.py +263 -0
  86. painapple_code/services/__init__.py +0 -0
  87. painapple_code/services/agent_session.py +2738 -0
  88. painapple_code/session_store.py +253 -0
  89. painapple_code/session_store_core.py +879 -0
  90. painapple_code/shadow_db.py +830 -0
  91. painapple_code/shadow_db_plans.py +369 -0
  92. painapple_code/shadow_db_queries.py +595 -0
  93. painapple_code/shadow_db_schema.py +676 -0
  94. painapple_code/shadow_git.py +1296 -0
  95. painapple_code/shadow_git_frontmatter.py +314 -0
  96. painapple_code/shadow_git_sections.py +520 -0
  97. painapple_code/shadow_git_summary.py +462 -0
  98. painapple_code/shadow_parser.py +560 -0
  99. painapple_code/static/css/00-variables.css +276 -0
  100. painapple_code/static/css/01-base.css +245 -0
  101. painapple_code/static/css/02-tooltips.css +36 -0
  102. painapple_code/static/css/09-left-rail.css +448 -0
  103. painapple_code/static/css/10-header.css +777 -0
  104. painapple_code/static/css/11-connection-bar.css +120 -0
  105. painapple_code/static/css/20-messages.css +750 -0
  106. painapple_code/static/css/21-tool-blocks.css +2340 -0
  107. painapple_code/static/css/22-edit-diff.css +619 -0
  108. painapple_code/static/css/23-thinking.css +1584 -0
  109. painapple_code/static/css/24-background-tasks.css +359 -0
  110. painapple_code/static/css/25-context-block.css +252 -0
  111. painapple_code/static/css/26-session-setup.css +144 -0
  112. painapple_code/static/css/30-markdown.css +854 -0
  113. painapple_code/static/css/40-input.css +2418 -0
  114. painapple_code/static/css/41-autocomplete.css +127 -0
  115. painapple_code/static/css/42-activity-strip.css +97 -0
  116. painapple_code/static/css/43-file-autocomplete.css +121 -0
  117. painapple_code/static/css/44-snippets-autocomplete.css +224 -0
  118. painapple_code/static/css/45-selection-bar.css +433 -0
  119. painapple_code/static/css/46-keyboard-bar.css +119 -0
  120. painapple_code/static/css/47-keyboard-longpress.css +116 -0
  121. painapple_code/static/css/47-token-profile.css +106 -0
  122. painapple_code/static/css/50-welcome.css +3095 -0
  123. painapple_code/static/css/51-modals.css +301 -0
  124. painapple_code/static/css/52-utilities.css +77 -0
  125. painapple_code/static/css/53-image-upload.css +456 -0
  126. painapple_code/static/css/54-todo-list.css +102 -0
  127. painapple_code/static/css/55-file-upload.css +113 -0
  128. painapple_code/static/css/56-chat-navigator.css +218 -0
  129. painapple_code/static/css/56-permission-card.css +332 -0
  130. painapple_code/static/css/56-plan-approval.css +204 -0
  131. painapple_code/static/css/56-question-form.css +602 -0
  132. painapple_code/static/css/57-swipe-indicator.css +95 -0
  133. painapple_code/static/css/58-session-families.css +627 -0
  134. painapple_code/static/css/59-about.css +196 -0
  135. painapple_code/static/css/61-log-explorer.css +723 -0
  136. painapple_code/static/css/62-json-tree.css +489 -0
  137. painapple_code/static/css/62-skills-widget.css +650 -0
  138. painapple_code/static/css/63-commands-widget.css +30 -0
  139. painapple_code/static/css/63-plugins-widget.css +174 -0
  140. painapple_code/static/css/63-terminal.css +516 -0
  141. painapple_code/static/css/64-search-files-widget.css +448 -0
  142. painapple_code/static/css/65-snippets-widget.css +426 -0
  143. painapple_code/static/css/66-active-sessions.css +979 -0
  144. painapple_code/static/css/66-agents-widget.css +265 -0
  145. painapple_code/static/css/66-file-explorer-widget.css +855 -0
  146. painapple_code/static/css/66-git-panel.css +791 -0
  147. painapple_code/static/css/66-history-explorer.css +1496 -0
  148. painapple_code/static/css/67-diff-viewer.css +602 -0
  149. painapple_code/static/css/67-editor-view.css +748 -0
  150. painapple_code/static/css/67-file-preview-widget.css +1873 -0
  151. painapple_code/static/css/68-browser-widget.css +337 -0
  152. painapple_code/static/css/68-compare-wizard.css +355 -0
  153. painapple_code/static/css/68-csv-preview.css +106 -0
  154. painapple_code/static/css/68-diff-preview.css +103 -0
  155. painapple_code/static/css/68-helpers-install-widget.css +491 -0
  156. painapple_code/static/css/68-image-annotate.css +346 -0
  157. painapple_code/static/css/68-terminal-view.css +98 -0
  158. painapple_code/static/css/68-uploads-widget.css +229 -0
  159. painapple_code/static/css/69-cost-analytics.css +537 -0
  160. painapple_code/static/css/70-config-panel.css +3736 -0
  161. painapple_code/static/css/70-excalidraw.css +136 -0
  162. painapple_code/static/css/70-prompt-explorer.css +890 -0
  163. painapple_code/static/css/71-chart.css +111 -0
  164. painapple_code/static/css/72-discussion-sidebar.css +678 -0
  165. painapple_code/static/css/74-debug-logs.css +656 -0
  166. painapple_code/static/css/80-open-dialog.css +146 -0
  167. painapple_code/static/css/80-quick-switcher.css +383 -0
  168. painapple_code/static/css/80-widget-system.css +1763 -0
  169. painapple_code/static/css/81-quick-actions.css +1020 -0
  170. painapple_code/static/css/82-context-popover.css +1233 -0
  171. painapple_code/static/css/83-context-menu.css +339 -0
  172. painapple_code/static/css/85-lazy-loading.css +102 -0
  173. painapple_code/static/css/86-zen-mode.css +708 -0
  174. painapple_code/static/css/87-grid-switcher.css +346 -0
  175. painapple_code/static/css/login.css +198 -0
  176. painapple_code/static/dist/app.bundle.js +7177 -0
  177. painapple_code/static/feature-triage.html +446 -0
  178. painapple_code/static/icons/apple-touch-icon.png +0 -0
  179. painapple_code/static/icons/favicon-32.png +0 -0
  180. painapple_code/static/icons/favicon.ico +0 -0
  181. painapple_code/static/icons/icon-128.png +0 -0
  182. painapple_code/static/icons/icon-144.png +0 -0
  183. painapple_code/static/icons/icon-152.png +0 -0
  184. painapple_code/static/icons/icon-180.png +0 -0
  185. painapple_code/static/icons/icon-192.png +0 -0
  186. painapple_code/static/icons/icon-384.png +0 -0
  187. painapple_code/static/icons/icon-512.png +0 -0
  188. painapple_code/static/icons/icon-72.png +0 -0
  189. painapple_code/static/icons/icon-96.png +0 -0
  190. painapple_code/static/js/action-delegate.js +187 -0
  191. painapple_code/static/js/activity-strip.js +193 -0
  192. painapple_code/static/js/app/context-menus.js +619 -0
  193. painapple_code/static/js/app/file-preview.js +163 -0
  194. painapple_code/static/js/app/init-ui.js +574 -0
  195. painapple_code/static/js/app/input-events.js +561 -0
  196. painapple_code/static/js/app/input-resize.js +313 -0
  197. painapple_code/static/js/app/message-actions.js +419 -0
  198. painapple_code/static/js/app/panels.js +210 -0
  199. painapple_code/static/js/app/rendering-delegators.js +246 -0
  200. painapple_code/static/js/app/server-loading.js +365 -0
  201. painapple_code/static/js/app/session-ops.js +287 -0
  202. painapple_code/static/js/app/session-switch.js +443 -0
  203. painapple_code/static/js/app/settings-helpers.js +280 -0
  204. painapple_code/static/js/app/startup-welcome.js +290 -0
  205. painapple_code/static/js/app-context.js +157 -0
  206. painapple_code/static/js/app.js +2134 -0
  207. painapple_code/static/js/auth-fetch.js +46 -0
  208. painapple_code/static/js/background-tasks.js +274 -0
  209. painapple_code/static/js/boot.js +73 -0
  210. painapple_code/static/js/caret-position.js +171 -0
  211. painapple_code/static/js/chat-navigator.js +710 -0
  212. painapple_code/static/js/chat-search.js +323 -0
  213. painapple_code/static/js/command-executor.js +176 -0
  214. painapple_code/static/js/command-store.js +497 -0
  215. painapple_code/static/js/components.js +1414 -0
  216. painapple_code/static/js/config.js +218 -0
  217. painapple_code/static/js/context-menu.js +740 -0
  218. painapple_code/static/js/controllers/chat-controller.js +4252 -0
  219. painapple_code/static/js/controllers/dialog-controller.js +217 -0
  220. painapple_code/static/js/controllers/tab-controller.js +1828 -0
  221. painapple_code/static/js/controllers/thinking-controller.js +915 -0
  222. painapple_code/static/js/diff-utils.js +486 -0
  223. painapple_code/static/js/drafts-pill.js +95 -0
  224. painapple_code/static/js/editor-view.js +815 -0
  225. painapple_code/static/js/effort-settings.js +430 -0
  226. painapple_code/static/js/feature-triage.js +811 -0
  227. painapple_code/static/js/file-autocomplete.js +516 -0
  228. painapple_code/static/js/file-tabs.js +123 -0
  229. painapple_code/static/js/gestures.js +448 -0
  230. painapple_code/static/js/grid-switcher.js +334 -0
  231. painapple_code/static/js/input-handler.js +1472 -0
  232. painapple_code/static/js/keyboard-bar.js +495 -0
  233. painapple_code/static/js/layout-switcher.js +109 -0
  234. painapple_code/static/js/linkify-utils.js +200 -0
  235. painapple_code/static/js/login.js +141 -0
  236. painapple_code/static/js/open-dialog.js +879 -0
  237. painapple_code/static/js/orphan-terminals.js +373 -0
  238. painapple_code/static/js/path-utils.js +179 -0
  239. painapple_code/static/js/perf-marks.js +102 -0
  240. painapple_code/static/js/permission-settings.js +475 -0
  241. painapple_code/static/js/pointer-input.js +59 -0
  242. painapple_code/static/js/preview/json-tree-menu.js +226 -0
  243. painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
  244. painapple_code/static/js/preview/json-tree.js +457 -0
  245. painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
  246. painapple_code/static/js/preview/preview-edit.js +367 -0
  247. painapple_code/static/js/preview/preview-events.js +361 -0
  248. painapple_code/static/js/preview/preview-history.js +567 -0
  249. painapple_code/static/js/preview/preview-inline-edit.js +869 -0
  250. painapple_code/static/js/preview/preview-poll.js +108 -0
  251. painapple_code/static/js/preview/preview-render.js +572 -0
  252. painapple_code/static/js/preview/preview-search.js +421 -0
  253. painapple_code/static/js/preview/preview-state.js +256 -0
  254. painapple_code/static/js/preview/preview-utils.js +251 -0
  255. painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
  256. painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
  257. painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
  258. painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
  259. painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
  260. painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
  261. painapple_code/static/js/preview-plugins/index.js +34 -0
  262. painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
  263. painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
  264. painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
  265. painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
  266. painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
  267. painapple_code/static/js/project-color-picker.js +110 -0
  268. painapple_code/static/js/project-colors.js +163 -0
  269. painapple_code/static/js/prompt-favorites.js +187 -0
  270. painapple_code/static/js/quick-actions-menu.js +1736 -0
  271. painapple_code/static/js/quick-actions-registry.js +1239 -0
  272. painapple_code/static/js/quick-switcher/controller.js +294 -0
  273. painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
  274. painapple_code/static/js/quick-switcher/index.js +30 -0
  275. painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
  276. painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
  277. painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
  278. painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
  279. painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
  280. painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
  281. painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
  282. painapple_code/static/js/quick-switcher/registry.js +69 -0
  283. painapple_code/static/js/quick-switcher/ui/item.js +54 -0
  284. painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
  285. painapple_code/static/js/recent-opens.js +40 -0
  286. painapple_code/static/js/scroll-manager.js +530 -0
  287. painapple_code/static/js/scroll-state-machine.js +359 -0
  288. painapple_code/static/js/selection/action-bar.js +793 -0
  289. painapple_code/static/js/selection/index.js +15 -0
  290. painapple_code/static/js/selection/selection-handler.js +877 -0
  291. painapple_code/static/js/selection/state.js +181 -0
  292. painapple_code/static/js/session/agent-progress.js +202 -0
  293. painapple_code/static/js/session/handle-agent-message.js +481 -0
  294. painapple_code/static/js/session/handle-message.js +550 -0
  295. painapple_code/static/js/session/interactive.js +587 -0
  296. painapple_code/static/js/session/message-store.js +271 -0
  297. painapple_code/static/js/session/messages.js +253 -0
  298. painapple_code/static/js/session/persistence.js +179 -0
  299. painapple_code/static/js/session/restore.js +136 -0
  300. painapple_code/static/js/session/sync.js +333 -0
  301. painapple_code/static/js/session-container-pool.js +371 -0
  302. painapple_code/static/js/session-setup-panel.js +274 -0
  303. painapple_code/static/js/session.js +1047 -0
  304. painapple_code/static/js/shortcut-hints.js +272 -0
  305. painapple_code/static/js/shortcuts.js +1146 -0
  306. painapple_code/static/js/skills-autocomplete.js +262 -0
  307. painapple_code/static/js/snippets-autocomplete.js +738 -0
  308. painapple_code/static/js/stash-refs-view.js +83 -0
  309. painapple_code/static/js/stash-ui.js +768 -0
  310. painapple_code/static/js/stash.js +785 -0
  311. painapple_code/static/js/status-bar.js +1184 -0
  312. painapple_code/static/js/token-profile.js +274 -0
  313. painapple_code/static/js/tool-renderer-blocks.js +1626 -0
  314. painapple_code/static/js/tool-renderer-thinking.js +813 -0
  315. painapple_code/static/js/tool-renderer.js +1500 -0
  316. painapple_code/static/js/tooltips.js +197 -0
  317. painapple_code/static/js/upload-manager.js +1019 -0
  318. painapple_code/static/js/utils.js +521 -0
  319. painapple_code/static/js/welcome/api.js +363 -0
  320. painapple_code/static/js/welcome/cards.js +626 -0
  321. painapple_code/static/js/welcome/context-menu.js +585 -0
  322. painapple_code/static/js/welcome/families.js +801 -0
  323. painapple_code/static/js/welcome/preview.js +234 -0
  324. painapple_code/static/js/welcome/state.js +129 -0
  325. painapple_code/static/js/welcome.js +1874 -0
  326. painapple_code/static/js/widget-system/base-widget.js +717 -0
  327. painapple_code/static/js/widget-system/device-manager.js +178 -0
  328. painapple_code/static/js/widget-system/event-bus.js +152 -0
  329. painapple_code/static/js/widget-system/icons.js +120 -0
  330. painapple_code/static/js/widget-system/index.js +49 -0
  331. painapple_code/static/js/widget-system/init.js +54 -0
  332. painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
  333. painapple_code/static/js/widget-system/types/floating.js +915 -0
  334. painapple_code/static/js/widget-system/types/index.js +40 -0
  335. painapple_code/static/js/widget-system/types/modal.js +147 -0
  336. painapple_code/static/js/widget-system/types/sidebar.js +158 -0
  337. painapple_code/static/js/widget-system/types/tab.js +146 -0
  338. painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
  339. painapple_code/static/js/widget-system/widget-manager.js +1187 -0
  340. painapple_code/static/js/widgets/about-widget.js +260 -0
  341. painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
  342. painapple_code/static/js/widgets/agents-widget.js +897 -0
  343. painapple_code/static/js/widgets/browser-widget.js +579 -0
  344. painapple_code/static/js/widgets/commands-widget.js +694 -0
  345. painapple_code/static/js/widgets/config/commit-sections.js +449 -0
  346. painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
  347. painapple_code/static/js/widgets/config/gestures.js +161 -0
  348. painapple_code/static/js/widgets/config/models-tab.js +988 -0
  349. painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
  350. painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
  351. painapple_code/static/js/widgets/config/state.js +517 -0
  352. painapple_code/static/js/widgets/config/system-controls.js +90 -0
  353. painapple_code/static/js/widgets/config-widget.js +1535 -0
  354. painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
  355. painapple_code/static/js/widgets/debug-widget.js +693 -0
  356. painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
  357. painapple_code/static/js/widgets/discussion-widget.js +1107 -0
  358. painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
  359. painapple_code/static/js/widgets/file-preview-widget.js +861 -0
  360. painapple_code/static/js/widgets/git-widget.js +794 -0
  361. painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
  362. painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
  363. painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
  364. painapple_code/static/js/widgets/image-preview-widget.js +692 -0
  365. painapple_code/static/js/widgets/index.js +176 -0
  366. painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
  367. painapple_code/static/js/widgets/plugins-widget.js +305 -0
  368. painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
  369. painapple_code/static/js/widgets/search-files-widget.js +529 -0
  370. painapple_code/static/js/widgets/skills-widget.js +843 -0
  371. painapple_code/static/js/widgets/snippets-widget.js +479 -0
  372. painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
  373. painapple_code/static/js/widgets/tasks-widget.js +379 -0
  374. painapple_code/static/js/widgets/terminal/connection.js +283 -0
  375. painapple_code/static/js/widgets/terminal/gestures.js +357 -0
  376. painapple_code/static/js/widgets/terminal/init.js +642 -0
  377. painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
  378. painapple_code/static/js/widgets/terminal/osc52.js +213 -0
  379. painapple_code/static/js/widgets/terminal/render.js +159 -0
  380. painapple_code/static/js/widgets/terminal/size.js +54 -0
  381. painapple_code/static/js/widgets/terminal/state.js +211 -0
  382. painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
  383. painapple_code/static/js/widgets/terminal-widget.js +793 -0
  384. painapple_code/static/js/widgets/uploads-widget.js +461 -0
  385. painapple_code/static/js/widgets/zen-widget.js +1001 -0
  386. painapple_code/static/login.html +51 -0
  387. painapple_code/static/manifest.json +73 -0
  388. painapple_code/static/sw.js +293 -0
  389. painapple_code/static/vendor/LICENSES.txt +769 -0
  390. painapple_code/static/vendor/README.md +103 -0
  391. painapple_code/static/vendor/codemirror.js +35 -0
  392. painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
  393. painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
  394. painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
  395. painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
  396. painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
  397. painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
  398. painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
  399. painapple_code/static/vendor/highlight.min.js +1213 -0
  400. painapple_code/static/vendor/marked.min.js +6 -0
  401. painapple_code/static/vendor/purify.min.js +3 -0
  402. painapple_code/static/vendor/xterm-addon-fit.js +2 -0
  403. painapple_code/static/vendor/xterm.css +209 -0
  404. painapple_code/static/vendor/xterm.js +2 -0
  405. painapple_code/static/web-client.html +635 -0
  406. painapple_code/subprocess_registry.py +535 -0
  407. painapple_code/tls_cert.py +79 -0
  408. painapple_code/tools/agents/shadow-git-helper.md +400 -0
  409. painapple_code/tools/excalidraw-to-svg.js +143 -0
  410. painapple_code/tools/install-helpers.sh +148 -0
  411. painapple_code/tools/shadow-git +320 -0
  412. painapple_code/tools/shadow-query +120 -0
  413. painapple_code/tools/vegalite-to-svg.js +79 -0
  414. painapple_code/turn_query.py +766 -0
  415. painapple_code/turn_tracker.py +221 -0
  416. painapple_code/utils/__init__.py +0 -0
  417. painapple_code/utils/agent_cli.py +306 -0
  418. painapple_code/utils/chart.py +103 -0
  419. painapple_code/utils/excalidraw.py +107 -0
  420. painapple_code/utils/file_lock.py +162 -0
  421. painapple_code/utils/file_paths.py +648 -0
  422. painapple_code/utils/generate_icons.py +251 -0
  423. painapple_code/utils/proc.py +205 -0
  424. painapple_code/utils/pty_backend.py +59 -0
  425. painapple_code/utils/pty_posix.py +160 -0
  426. painapple_code/utils/pty_windows.py +245 -0
  427. painapple_code/utils/token_profiles.py +91 -0
  428. painapple_code/viewer_templates.py +657 -0
  429. painapple_code/welcome_search.py +879 -0
  430. painapple_code-1.0.0.dist-info/METADATA +292 -0
  431. painapple_code-1.0.0.dist-info/RECORD +436 -0
  432. painapple_code-1.0.0.dist-info/WHEEL +5 -0
  433. painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
  434. painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
  435. painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
  436. 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
+ }