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,321 @@
1
+ """Codex provider — declarative surface + customization roots.
2
+
3
+ What the Codex adapter *declares about itself*: binary location, availability,
4
+ effort/permission vocabularies, and where its skills/agents/plugins live. The
5
+ `CodexProvider` combiner in this package's ``__init__`` mixes this in. Kept
6
+ separate from launch/translate so the provider's static description reads in one
7
+ place.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from typing import Optional
14
+
15
+ from painapple_code import paths
16
+ from painapple_code.providers.base import PluginBackend
17
+
18
+ # Context-window size used to meter the context bar. `codex exec --json` reports
19
+ # per-turn token usage but — unlike the TUI / app-server protocol — emits NO
20
+ # window size and has no `/context` command, so we supply it. The gpt-5-codex
21
+ # family is metered by Codex against a 272K-token *input* window (the input split
22
+ # of the 400K product window; Codex then applies a ~95% effective cap on top —
23
+ # see openai/codex#19319, #19409). The app doesn't manage which Codex model is
24
+ # active (models() is empty — it's chosen by `codex login` / config.toml), so we
25
+ # meter against this family default rather than a per-model lookup. When a real
26
+ # Codex model catalog with per-model windows is registered, context_from_result
27
+ # can switch to a model-keyed size with no other change.
28
+ _DEFAULT_CONTEXT_WINDOW = 272_000
29
+
30
+ # Canonical ReasoningEffort ordering — used to order the union vocabulary and
31
+ # to clamp a requested level down to the nearest one a model supports. The
32
+ # per-model truth is the CLI's own models_cache (supported_reasoning_levels).
33
+ _EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]
34
+ # Pre-cache fallback (codex installed but never run): the classic triad every
35
+ # codex model speaks.
36
+ _FALLBACK_EFFORTS = ["low", "medium", "high"]
37
+
38
+
39
+ class _CapabilitiesMixin:
40
+ """Provider self-description: binary, availability, vocabularies, roots."""
41
+
42
+ # --- binary / availability -------------------------------------------
43
+
44
+ # Settings "CLI path" row + generic engine-path endpoint (shared by both
45
+ # Codex drivers — same binary). Model definitions stay read-only in
46
+ # Settings — the Codex CLI owns them (models_cache.json) — but per-model
47
+ # show/hide toggles apply, namespaced under the shared "codex" key so
48
+ # both drivers see the same curation.
49
+ path_config_key = "codex_path"
50
+ default_binary = "codex"
51
+ # "Continue in CLI" quick action → `codex exec resume <thread-id>` (both
52
+ # Codex drivers resume the same rollout by thread id; codex_app_server
53
+ # inherits this mixin).
54
+ cli_resume_template = "codex exec resume {id}"
55
+ models_key = "codex"
56
+ # `codex login status` exits 0/1 with a one-line status (base parser
57
+ # handles it — note codex prints to stderr). Login uses the device-code
58
+ # flow: plain `codex login` binds a localhost callback on the SERVER,
59
+ # which a remote client can never reach; --device-auth prints a URL +
60
+ # code that work from any device. (Caution: starting either flow drops
61
+ # the existing auth.json before the new login completes.)
62
+ auth_status_args = ["login", "status"]
63
+ auth_login_args = ["login", "--device-auth"]
64
+ # Auto-journal: both Codex drivers self-summarize on the Codex side and
65
+ # share the optional cheaper-summarizer override; empty = the summary
66
+ # fork inherits the session thread's model.
67
+ summary_model_config_key = "codex_summary_model"
68
+ summary_model_placeholder = "session model"
69
+
70
+ def parse_auth_status(self, returncode: int, stdout: str, stderr: str) -> dict:
71
+ parsed = super().parse_auth_status(returncode, stdout, stderr)
72
+ if not parsed.get("logged_in"):
73
+ # Codex's logged-out line is literally "Not logged in" — the UI
74
+ # already says that; an echoed detail would just duplicate it.
75
+ parsed["detail"] = ""
76
+ return parsed
77
+
78
+ def binary(self) -> str:
79
+ return paths.load_global_config().get(
80
+ self.path_config_key, self.default_binary)
81
+
82
+ def is_available(self) -> tuple[bool, Optional[str]]:
83
+ """Codex needs both the CLI on PATH and an authenticated `codex login`.
84
+
85
+ Auth lives in `$CODEX_HOME/auth.json` (or `~/.codex/`); an API key in the
86
+ environment also works. We only block when there's *no* sign of setup —
87
+ a present config dir is treated as good enough so we don't false-negative.
88
+ """
89
+ ok, reason = super().is_available()
90
+ if not ok:
91
+ return False, "Codex CLI not installed — `npm i -g @openai/codex`"
92
+ from pathlib import Path
93
+ codex_home = Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
94
+ authed = (
95
+ (codex_home / "auth.json").exists()
96
+ or (codex_home / "config.toml").exists()
97
+ or codex_home.is_dir()
98
+ or bool(os.environ.get("CODEX_API_KEY") or os.environ.get("OPENAI_API_KEY"))
99
+ )
100
+ if not authed:
101
+ return False, "Codex not signed in — run `codex login`"
102
+ return True, None
103
+
104
+ def _listed_models_cache(self) -> list[dict]:
105
+ """Raw ``visibility=list`` entries from the CLI's models cache,
106
+ priority-sorted. Shared source for models() / effort_levels() /
107
+ effort_for_model(). Missing/malformed cache → empty list."""
108
+ import json
109
+ from pathlib import Path
110
+ codex_home = Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
111
+ try:
112
+ with (codex_home / "models_cache.json").open(encoding="utf-8") as f:
113
+ data = json.load(f)
114
+ except (OSError, json.JSONDecodeError):
115
+ return []
116
+ entries = data.get("models") if isinstance(data, dict) else None
117
+ if not isinstance(entries, list):
118
+ return []
119
+ listed = [
120
+ m for m in entries
121
+ if isinstance(m, dict) and m.get("visibility") == "list" and m.get("slug")
122
+ ]
123
+ listed.sort(key=lambda m: m.get("priority") or 0)
124
+ return listed
125
+
126
+ @staticmethod
127
+ def _model_efforts(entry: dict) -> list[str]:
128
+ """One cache entry's ``supported_reasoning_levels`` → effort names."""
129
+ out: list[str] = []
130
+ for lvl in entry.get("supported_reasoning_levels") or []:
131
+ effort = lvl.get("effort") if isinstance(lvl, dict) else None
132
+ if isinstance(effort, str) and effort:
133
+ out.append(effort)
134
+ return out
135
+
136
+ def models(self) -> list[dict]:
137
+ """Codex's own model catalog, read from the CLI's cache.
138
+
139
+ The Codex CLI maintains ``$CODEX_HOME/models_cache.json`` (refreshed
140
+ from the backend's model registry on every run): slug, display name,
141
+ description, visibility, priority and supported reasoning levels per
142
+ model. Surfacing that file — rather than a hardcoded list — keeps the
143
+ picker in lockstep with whatever this Codex install actually offers.
144
+ Missing/malformed cache (codex installed but never run) → empty
145
+ catalog, and the UI shows only the engine-default option.
146
+
147
+ The launch paths guard on this: exec passes ``-m`` only for ids listed
148
+ here; the app-server forwards any non-Claude id (so an explicit pick
149
+ still works even while the cache is absent). With a catalog present the
150
+ app OWNS the default — `paths.engine_default_model` resolves the
151
+ top (priority-first) listed model and the launch forwards it, so a
152
+ fresh session runs a concrete model rather than deferring to the CLI's
153
+ own config. Only an ABSENT catalog (no cache) falls back to
154
+ `codex login` / config.toml (and the picker/chip hide entirely).
155
+
156
+ Each entry carries ``efforts`` — the model's OWN reasoning-effort
157
+ range — so pickers can narrow the engine-level union to the selected
158
+ model (launch clamps to the same list via effort_for_model).
159
+ """
160
+ return [
161
+ {
162
+ "id": m["slug"],
163
+ "label": m.get("display_name") or m["slug"],
164
+ "desc": m.get("description") or "",
165
+ "efforts": [
166
+ lvl for lvl in _EFFORT_ORDER
167
+ if lvl in self._model_efforts(m)
168
+ ],
169
+ }
170
+ for m in self._listed_models_cache()
171
+ ]
172
+
173
+ # --- effort / permissions --------------------------------------------
174
+
175
+ def effort_levels(self) -> list[str]:
176
+ """Codex's effort vocabulary, read from the CLI's own model registry.
177
+
178
+ ``models_cache.json`` self-describes ``supported_reasoning_levels``
179
+ per model (the 5.6 family reaches xhigh/max/ultra; older models stop
180
+ at xhigh). The engine-level vocabulary is the ordered union across
181
+ the listed catalog — the offering is wide, and ``effort_for_model``
182
+ clamps at launch to what the *target* model actually supports.
183
+ Missing cache → the classic triad every codex model speaks.
184
+ """
185
+ union: set = set()
186
+ for entry in self._listed_models_cache():
187
+ union.update(self._model_efforts(entry))
188
+ ordered = [lvl for lvl in _EFFORT_ORDER if lvl in union]
189
+ return ordered or list(_FALLBACK_EFFORTS)
190
+
191
+ def effort_for_model(self, effort: Optional[str], model: Optional[str]) -> Optional[str]:
192
+ """Clamp a requested reasoning effort to the target model's own range.
193
+
194
+ The wire value must be one the model actually supports — the CLI's
195
+ registry is authoritative, and an out-of-range value risks a rejected
196
+ turn. Known model → its ``supported_reasoning_levels``; unknown or
197
+ default model → the intersection every listed model speaks (still
198
+ cache-driven); no cache → the classic triad. A level above the range
199
+ clamps down to the highest supported one (``ultra`` → ``max`` on a
200
+ model that stops there; legacy Claude ``max`` → ``xhigh`` where that's
201
+ the top); one below clamps up to the lowest. Returns None when no
202
+ effort was requested (or the value is garbage) — codex then runs its
203
+ own default.
204
+ """
205
+ if not effort or effort not in _EFFORT_ORDER:
206
+ return None
207
+ entries = self._listed_models_cache()
208
+ supported: list[str] = []
209
+ if entries:
210
+ if model:
211
+ entry = next((m for m in entries if m.get("slug") == model), None)
212
+ if entry:
213
+ supported = [
214
+ lvl for lvl in _EFFORT_ORDER
215
+ if lvl in self._model_efforts(entry)
216
+ ]
217
+ if not supported:
218
+ sets = [s for s in (set(self._model_efforts(m)) for m in entries) if s]
219
+ common = set.intersection(*sets) if sets else set()
220
+ supported = [lvl for lvl in _EFFORT_ORDER if lvl in common]
221
+ if not supported:
222
+ supported = list(_FALLBACK_EFFORTS)
223
+ if effort in supported:
224
+ return effort
225
+ idx = _EFFORT_ORDER.index(effort)
226
+ below = [lvl for lvl in supported if _EFFORT_ORDER.index(lvl) <= idx]
227
+ return below[-1] if below else supported[0]
228
+
229
+ def permission_modes(self) -> list[dict]:
230
+ # Codex's OWN native sandbox vocabulary — the provider owns this list and
231
+ # the session stores these values directly (no Claude-vocabulary
232
+ # indirection). `color` lets the UI render each tier without knowing
233
+ # Codex's scheme. _SANDBOX_BY_MODE (launch.py) passes them to `--sandbox`.
234
+ return [
235
+ {"value": "read-only", "label": "Read-only",
236
+ "desc": "Sandboxed — no file writes", "color": "#8b5cf6"},
237
+ {"value": "workspace-write", "label": "Workspace write",
238
+ "desc": "Edit files in the workspace", "color": "#fb923c"},
239
+ {"value": "danger-full-access", "label": "Full access",
240
+ "desc": "No sandbox — full disk & network", "color": "#f87171"},
241
+ ]
242
+
243
+ def default_permission_mode(self) -> str:
244
+ # A fresh Codex session should be able to do work but stay sandboxed —
245
+ # not inherit Claude's global default (e.g. 'dontAsk' → read-only).
246
+ return "workspace-write"
247
+
248
+ # --- customization roots (skills / plugins / agents) ------------------
249
+
250
+ def skill_roots(self, cwd: str) -> list[dict]:
251
+ # Codex uses the same folder-form SKILL.md format as Claude, only the
252
+ # locations differ. `.agents/skills` is the preferred cross-agent
253
+ # convention; `.codex/skills` is the still-supported repo layout; the
254
+ # CODEX_HOME location is deprecated-but-supported for the user scope.
255
+ # Listed highest-priority first — the first project/personal entry is
256
+ # also where a new skill of that scope is created.
257
+ from pathlib import Path
258
+ home = Path.home()
259
+ codex_home = Path(os.environ.get("CODEX_HOME") or home / ".codex")
260
+ return [
261
+ {"scope": "project", "dir": Path(cwd) / ".agents" / "skills"},
262
+ {"scope": "project", "dir": Path(cwd) / ".codex" / "skills"},
263
+ {"scope": "personal", "dir": home / ".agents" / "skills"},
264
+ {"scope": "personal", "dir": codex_home / "skills"},
265
+ ]
266
+
267
+ def plugin_backend(self) -> PluginBackend:
268
+ # `codex plugin list --json` → {installed, available} (same shape as
269
+ # Claude); `codex plugin add/remove` install/uninstall. Codex has no
270
+ # enable/disable (the UI hides the toggle). Inventory is skipped: Codex
271
+ # caches plugins under an unstable ~/.codex/.tmp snapshot dir, and bundles
272
+ # components differently — not worth walking here.
273
+ return PluginBackend(
274
+ binary=self.binary(),
275
+ list_installed_args=["plugin", "list", "--json"],
276
+ list_all_args=["plugin", "list", "--json"],
277
+ install_verb=["plugin", "add"],
278
+ uninstall_verb=["plugin", "remove"],
279
+ enable_verb=None,
280
+ disable_verb=None,
281
+ marketplace_root=None,
282
+ )
283
+
284
+ def agent_roots(self, cwd: str) -> list[dict]:
285
+ # Codex agents are TOML (`<name>.toml`): project `.codex/agents`, user
286
+ # `~/.codex/agents` (or $CODEX_HOME/agents). Read-only in the browser for
287
+ # now — the schema (developer_instructions / model_reasoning_effort /
288
+ # sandbox_mode) and TOML round-trip differ from Claude's markdown agents;
289
+ # editing is a separate follow-up. (Built-in roles default/worker/explorer
290
+ # have no file and aren't listed, mirroring Claude's built-in subagents.)
291
+ from pathlib import Path
292
+ home = Path.home()
293
+ codex_home = Path(os.environ.get("CODEX_HOME") or home / ".codex")
294
+ return [
295
+ {"scope": "project", "dir": Path(cwd) / ".codex" / "agents",
296
+ "fmt": "toml", "writable": False},
297
+ {"scope": "personal", "dir": codex_home / "agents",
298
+ "fmt": "toml", "writable": False},
299
+ ]
300
+
301
+ # --- context metering -------------------------------------------------
302
+
303
+ def context_from_result(self, result_msg: dict, model: Optional[str] = None) -> Optional[dict]:
304
+ # Codex has no /context command, but turn.completed.usage.input_tokens is
305
+ # the full conversation context fed to the model that turn — i.e. the live
306
+ # window occupancy, which is exactly what Codex's own TUI meters. The
307
+ # canonical result (built by _result_from_usage) carries it as
308
+ # usage.input_tokens; cached tokens are a *subset* of that, so we don't add
309
+ # them. No per-section breakdown exists in exec mode.
310
+ usage = (result_msg or {}).get("usage") or {}
311
+ in_tok = usage.get("input_tokens") or 0
312
+ if not in_tok:
313
+ return None
314
+ window = _DEFAULT_CONTEXT_WINDOW
315
+ return {
316
+ "contextTokens": in_tok,
317
+ "contextWindow": window,
318
+ "percentage": round(in_tok / window * 100, 1) if window else None,
319
+ "breakdown": None,
320
+ "memoryFiles": None,
321
+ }
@@ -0,0 +1,70 @@
1
+ """Codex provider — stderr classification and retry/termination policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from painapple_code.providers.base import StderrClass
6
+
7
+ # Retryable transient-error substrings on Codex stderr / result text.
8
+ _RETRYABLE_SUBSTRINGS = (
9
+ "rate limit", "429", "stream error", "503", "502", "500",
10
+ "overloaded", "timed out", "connection reset",
11
+ )
12
+ # Codex resume of a thread that no longer exists.
13
+ _STALE_SUBSTRINGS = ("no rollout found", "session not found", "thread not found")
14
+
15
+ # ChatGPT-plan / API usage-limit one-liners on turn.failed / error events.
16
+ _USAGE_LIMIT_SUBSTRINGS = ("usage limit", "hit your limit", "rate limit reached")
17
+
18
+ # Auth failures the user can fix by re-logging in (`codex login`).
19
+ _AUTH_SUBSTRINGS = (
20
+ "401", "unauthorized", "token expired", "not logged in",
21
+ "login required", "invalid api key", "authentication",
22
+ )
23
+
24
+
25
+ class _ErrorsMixin:
26
+ """Maps Codex stderr/result text to the canonical error classes."""
27
+
28
+ def classify_stderr(self, line: str) -> StderrClass:
29
+ low = line.lower()
30
+ if any(s in low for s in _STALE_SUBSTRINGS):
31
+ return StderrClass.STALE_SESSION
32
+ if any(s in low for s in _RETRYABLE_SUBSTRINGS):
33
+ return StderrClass.RETRYABLE
34
+ return StderrClass.NONE
35
+
36
+ def is_retryable_api_error(self, text: str) -> bool:
37
+ low = text.lower()
38
+ # Auth failures need a re-login, not a resend — without this guard a
39
+ # message containing both ("401 ... stream error") would raise the
40
+ # re-login card AND kick off a doomed server-level retry loop.
41
+ if any(s in low for s in _AUTH_SUBSTRINGS):
42
+ return False
43
+ return any(s in low for s in _RETRYABLE_SUBSTRINGS)
44
+
45
+ def is_usage_limit(self, text: str) -> bool:
46
+ # Genuine limit reports are one-liners; the length gate keeps prose that
47
+ # merely quotes a limit phrase from ever matching (same contract as the
48
+ # Claude provider's matcher).
49
+ if not text or len(text) > 200:
50
+ return False
51
+ low = text.lower()
52
+ return any(s in low for s in _USAGE_LIMIT_SUBSTRINGS)
53
+
54
+ def is_auth_error(self, text: str, api_error_status=None) -> bool:
55
+ if api_error_status == 401:
56
+ return True
57
+ if not text or len(text) > 300:
58
+ return False
59
+ low = text.lower()
60
+ return any(s in low for s in _AUTH_SUBSTRINGS)
61
+
62
+ def binary_not_found_hint(self) -> str:
63
+ return ("Install the Codex CLI (`npm i -g @openai/codex`) or point the "
64
+ "CLI path at it in Settings → Engines (`codex_path` config key).")
65
+
66
+ @property
67
+ def normal_termination_codes(self) -> frozenset:
68
+ # A finished `codex exec` turn exits 0; SIGINT/SIGTERM stops map to
69
+ # 130/143 like any subprocess.
70
+ return frozenset({0, 130, 143})
@@ -0,0 +1,142 @@
1
+ """Codex provider — command building and image materialization.
2
+
3
+ Turns a `LaunchOptions` into the `codex exec [resume <id>] --json …` argv,
4
+ including the effort→`model_reasoning_effort` and permission→`--sandbox`
5
+ mappings. Mixed into `CodexProvider`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from painapple_code import paths
13
+ from painapple_code.providers.base import LaunchOptions
14
+
15
+ # Effort → `model_reasoning_effort` goes through capabilities'
16
+ # effort_for_model(): the CLI's own models_cache declares each model's
17
+ # supported reasoning levels, and the requested level is clamped to the
18
+ # TARGET model's range (unknown/default model → the range every listed
19
+ # model speaks; no cache → the classic low/medium/high triad).
20
+
21
+ # Permission value → codex `--sandbox` policy. `codex exec` has no approval flag
22
+ # (it runs non-interactively with approvals off); the sandbox policy alone
23
+ # governs what model-generated commands may do. read-only blocks all writes;
24
+ # workspace-write allows in-workspace edits; danger-full-access removes the
25
+ # sandbox entirely.
26
+ #
27
+ # New sessions store Codex's OWN native vocabulary (see permission_modes), so
28
+ # those map to themselves. The Claude-vocabulary rows are a back-compat shim for
29
+ # sessions created before Codex registered native modes — provider is frozen
30
+ # after the first turn, so such a session keeps its stored canonical value.
31
+ _SANDBOX_BY_MODE = {
32
+ # Codex-native (identity)
33
+ "read-only": "read-only",
34
+ "workspace-write": "workspace-write",
35
+ "danger-full-access": "danger-full-access",
36
+ # Legacy Claude-vocabulary (pre-native sessions)
37
+ "plan": "read-only",
38
+ "dontAsk": "read-only",
39
+ "acceptEdits": "workspace-write",
40
+ "auto": "workspace-write",
41
+ "bypassPermissions": "danger-full-access",
42
+ }
43
+ # An unrecognised / unset mode: let the agent do work but stay sandboxed.
44
+ _DEFAULT_SANDBOX = "workspace-write"
45
+
46
+
47
+ class _LaunchMixin:
48
+ """Builds the per-turn `codex exec` command line."""
49
+
50
+ def frame_input(self, message: dict) -> dict:
51
+ # Never called: `codex exec` is ephemeral (persistent_process=False) —
52
+ # the turn's prompt rides in argv via LaunchOptions.prompt, so nothing
53
+ # is ever framed for stdin. Raise so a mis-wired send fails loudly.
54
+ raise NotImplementedError(
55
+ "codex exec embeds the prompt in argv; it has no stdin protocol")
56
+
57
+ def build_command(self, opts: LaunchOptions) -> list[str]:
58
+ cmd = [self.binary(), "exec"]
59
+
60
+ # Resume an existing thread, else start a fresh one.
61
+ if opts.session_id:
62
+ cmd += ["resume", opts.session_id]
63
+
64
+ cmd += ["--json", "--skip-git-repo-check"]
65
+
66
+ # Pass `-m` only for a model THIS engine actually offers; otherwise let
67
+ # Codex use its configured default (from `codex login` /
68
+ # ~/.codex/config.toml). The session's model hierarchy defaults to a
69
+ # *Claude* id, which isn't in Codex's catalog, so it's never forwarded —
70
+ # and when a real Codex catalog is registered, `-m` starts working with
71
+ # no change here.
72
+ forwarded_model = None
73
+ if opts.model and any(m.get("id") == opts.model for m in self.models()):
74
+ forwarded_model = opts.model
75
+ cmd += ["-m", opts.model]
76
+
77
+ if opts.effort:
78
+ # Clamp to what the model that will actually run supports (the
79
+ # configured default when no -m is forwarded → conservative range).
80
+ mapped = self.effort_for_model(opts.effort, forwarded_model)
81
+ if mapped:
82
+ # The config key is `model_reasoning_effort` — a bare
83
+ # `reasoning_effort` override is silently ignored by Codex.
84
+ cmd += ["-c", f"model_reasoning_effort={mapped}"]
85
+
86
+ # Sandbox policy. The top-level `codex exec` takes `-s <mode>`, but the
87
+ # `resume` subcommand has no `-s` flag — there the policy is a config
88
+ # override (`-c sandbox_mode=...`, parsed as TOML / raw string).
89
+ sandbox = _SANDBOX_BY_MODE.get(opts.permission_mode or "", _DEFAULT_SANDBOX)
90
+ if opts.session_id:
91
+ cmd += ["-c", f'sandbox_mode="{sandbox}"']
92
+ else:
93
+ cmd += ["-s", sandbox]
94
+
95
+ # Images: write base64 uploads to temp files and pass their paths.
96
+ image_paths = self._materialize_images(opts.images)
97
+ if image_paths:
98
+ cmd += ["-i", ",".join(image_paths)]
99
+
100
+ # Prompt is the final positional argument. Empty string is valid
101
+ # (e.g. an API-retry "continue" turn with no new text).
102
+ cmd.append(opts.prompt or "")
103
+
104
+ return cmd
105
+
106
+ @staticmethod
107
+ def _materialize_images(images: Optional[list]) -> list[str]:
108
+ """Decode base64 upload objects to temp files; return their paths."""
109
+ if not images:
110
+ return []
111
+ import base64
112
+ import tempfile
113
+ import uuid
114
+ out_dir = paths.DATA_HOME / "tmp" / "codex-images"
115
+ try:
116
+ out_dir.mkdir(parents=True, exist_ok=True)
117
+ except OSError:
118
+ out_dir = None
119
+ paths: list[str] = []
120
+ for img in images:
121
+ source = img.get("source", {}) if isinstance(img, dict) else {}
122
+ if source.get("type") != "base64" or not source.get("data"):
123
+ continue
124
+ media_type = source.get("media_type", "image/png")
125
+ ext = "jpg" if "jpeg" in media_type else media_type.split("/")[-1]
126
+ name = f"img_{uuid.uuid4().hex[:10]}.{ext}"
127
+ try:
128
+ data = base64.b64decode(source["data"])
129
+ if out_dir is not None:
130
+ p = out_dir / name
131
+ p.write_bytes(data)
132
+ else: # fall back to the system temp dir
133
+ fd = tempfile.NamedTemporaryFile(
134
+ prefix="codex_img_", suffix=f".{ext}", delete=False
135
+ )
136
+ fd.write(data)
137
+ fd.close()
138
+ p = fd.name
139
+ paths.append(str(p))
140
+ except (OSError, ValueError):
141
+ continue
142
+ return paths