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