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,676 @@
1
+ """Schema DDL, migrations, and FTS index management for ShadowDB.
2
+
3
+ Mixed into ShadowDB. Owns SCHEMA_SQL and the system field definitions.
4
+ Methods here run at DB init time, on recovery, or to refresh FTS state.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+
10
+ import duckdb
11
+
12
+ from painapple_code import paths
13
+
14
+ logger = logging.getLogger("painapple-code.shadow-db")
15
+
16
+ # ═══════════════════════════════════════════════════════════════════════════
17
+ # System field definitions (seeded on init)
18
+ # ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ SYSTEM_FIELD_DEFS = [
21
+ {"key": "summary", "label": "Summary", "field_type": "text",
22
+ "prompt": "One sentence: what was accomplished", "display_order": 1},
23
+ {"key": "work_done", "label": "Work Done", "field_type": "list",
24
+ "prompt": "Bullet points of specific changes", "display_order": 2},
25
+ {"key": "learnings", "label": "Learnings", "field_type": "list",
26
+ "prompt": "Key insights discovered", "display_order": 3},
27
+ {"key": "context_for_resume", "label": "Context", "field_type": "text",
28
+ "prompt": "What someone needs to continue this work", "display_order": 4},
29
+ {"key": "commands", "label": "Commands", "field_type": "list",
30
+ "prompt": "Reusable shell commands worth remembering", "display_order": 6},
31
+ ]
32
+
33
+
34
+ # ═══════════════════════════════════════════════════════════════════════════
35
+ # Schema DDL
36
+ # ═══════════════════════════════════════════════════════════════════════════
37
+
38
+ SCHEMA_SQL = """
39
+ -- Sessions: container for turns
40
+ CREATE TABLE IF NOT EXISTS sessions (
41
+ id VARCHAR PRIMARY KEY,
42
+ project_hash VARCHAR NOT NULL,
43
+ git_repo_hash VARCHAR,
44
+ name VARCHAR,
45
+ created_at TIMESTAMPTZ,
46
+ updated_at TIMESTAMPTZ,
47
+ model VARCHAR,
48
+ archived BOOLEAN DEFAULT false,
49
+ total_cost DECIMAL(10,4) DEFAULT 0,
50
+ turn_count INTEGER DEFAULT 0,
51
+ provider VARCHAR DEFAULT 'claude' -- CLI agent backing the session
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_hash);
55
+ CREATE INDEX IF NOT EXISTS idx_sessions_repo ON sessions(git_repo_hash);
56
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
57
+
58
+ -- Turns: THE HERO TABLE
59
+ CREATE TABLE IF NOT EXISTS turns (
60
+ id VARCHAR PRIMARY KEY,
61
+ session_id VARCHAR NOT NULL,
62
+ project_hash VARCHAR NOT NULL,
63
+ git_repo_hash VARCHAR,
64
+ turn_number INTEGER,
65
+ status VARCHAR DEFAULT 'pending',
66
+
67
+ -- Prompt (captured immediately)
68
+ user_prompt VARCHAR NOT NULL,
69
+ has_images BOOLEAN DEFAULT false,
70
+ has_files BOOLEAN DEFAULT false,
71
+ started_at TIMESTAMPTZ DEFAULT now(),
72
+
73
+ -- Git context (captured on send)
74
+ git_branch VARCHAR,
75
+ shadow_branch VARCHAR,
76
+
77
+ -- Completion (filled after turn finishes)
78
+ completed_at TIMESTAMPTZ,
79
+ git_hash VARCHAR,
80
+ cost DECIMAL(10,4),
81
+ summary_cost DECIMAL(10,4),
82
+ tokens_in UINTEGER,
83
+ tokens_out UINTEGER,
84
+ duration_ms INTEGER,
85
+ model VARCHAR,
86
+
87
+ -- Provider + result metadata
88
+ provider VARCHAR DEFAULT 'claude', -- CLI agent backing the turn
89
+ provider_session_id VARCHAR,
90
+ provider_turn_uuid VARCHAR,
91
+ result_subtype VARCHAR,
92
+ is_error BOOLEAN DEFAULT false,
93
+ num_tool_loops INTEGER,
94
+ duration_api_ms INTEGER,
95
+ service_tier VARCHAR,
96
+
97
+ -- Cache efficiency
98
+ cache_read_tokens UINTEGER,
99
+ cache_creation_tokens UINTEGER,
100
+
101
+ -- Web tool usage
102
+ web_search_count INTEGER DEFAULT 0,
103
+ web_fetch_count INTEGER DEFAULT 0,
104
+
105
+ -- Error tracking
106
+ error_message VARCHAR,
107
+ permission_denials VARCHAR[]
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);
111
+ CREATE INDEX IF NOT EXISTS idx_turns_project ON turns(project_hash);
112
+ CREATE INDEX IF NOT EXISTS idx_turns_repo ON turns(git_repo_hash);
113
+ CREATE INDEX IF NOT EXISTS idx_turns_started ON turns(started_at DESC);
114
+ CREATE INDEX IF NOT EXISTS idx_turns_status ON turns(status);
115
+ CREATE INDEX IF NOT EXISTS idx_turns_git ON turns(git_hash);
116
+ CREATE INDEX IF NOT EXISTS idx_turns_branch ON turns(git_branch);
117
+ CREATE INDEX IF NOT EXISTS idx_turns_provider_session ON turns(provider_session_id);
118
+
119
+ -- Field definitions (system + user)
120
+ CREATE TABLE IF NOT EXISTS field_defs (
121
+ id INTEGER PRIMARY KEY,
122
+ project_hash VARCHAR,
123
+ key VARCHAR NOT NULL,
124
+ label VARCHAR NOT NULL,
125
+ field_type VARCHAR NOT NULL,
126
+ prompt VARCHAR,
127
+ display_order INTEGER DEFAULT 0,
128
+ source VARCHAR DEFAULT 'user',
129
+ enabled BOOLEAN DEFAULT true,
130
+ created_at TIMESTAMPTZ DEFAULT now(),
131
+ UNIQUE(project_hash, key)
132
+ );
133
+
134
+ -- Turn fields: one row per field per turn
135
+ CREATE TABLE IF NOT EXISTS turn_fields (
136
+ id INTEGER PRIMARY KEY,
137
+ turn_id VARCHAR NOT NULL,
138
+ field_key VARCHAR NOT NULL,
139
+ value_text VARCHAR,
140
+ value_list VARCHAR[],
141
+ search_text VARCHAR,
142
+ edited BOOLEAN DEFAULT false,
143
+ UNIQUE(turn_id, field_key)
144
+ );
145
+
146
+ CREATE INDEX IF NOT EXISTS idx_turn_fields_turn ON turn_fields(turn_id);
147
+ CREATE INDEX IF NOT EXISTS idx_turn_fields_key ON turn_fields(field_key);
148
+
149
+ -- Tags
150
+ CREATE TABLE IF NOT EXISTS tags (
151
+ id INTEGER PRIMARY KEY,
152
+ turn_id VARCHAR NOT NULL,
153
+ tag VARCHAR NOT NULL,
154
+ source VARCHAR DEFAULT 'auto',
155
+ created_at TIMESTAMPTZ DEFAULT now(),
156
+ UNIQUE(turn_id, tag)
157
+ );
158
+
159
+ CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
160
+ CREATE INDEX IF NOT EXISTS idx_tags_turn ON tags(turn_id);
161
+
162
+ -- Files changed per turn
163
+ CREATE TABLE IF NOT EXISTS turn_files (
164
+ id INTEGER PRIMARY KEY,
165
+ turn_id VARCHAR NOT NULL,
166
+ file_path VARCHAR NOT NULL,
167
+ change_type VARCHAR,
168
+ UNIQUE(turn_id, file_path)
169
+ );
170
+
171
+ CREATE INDEX IF NOT EXISTS idx_turn_files_path ON turn_files(file_path);
172
+ CREATE INDEX IF NOT EXISTS idx_turn_files_turn ON turn_files(turn_id);
173
+
174
+ -- Tools used per turn
175
+ CREATE TABLE IF NOT EXISTS turn_tools (
176
+ id INTEGER PRIMARY KEY,
177
+ turn_id VARCHAR NOT NULL,
178
+ tool_name VARCHAR NOT NULL,
179
+ call_count INTEGER DEFAULT 1,
180
+ UNIQUE(turn_id, tool_name)
181
+ );
182
+
183
+ CREATE INDEX IF NOT EXISTS idx_turn_tools_name ON turn_tools(tool_name);
184
+ CREATE INDEX IF NOT EXISTS idx_turn_tools_turn ON turn_tools(turn_id);
185
+
186
+ -- Per-model token/cost breakdown
187
+ CREATE TABLE IF NOT EXISTS turn_model_usage (
188
+ id INTEGER PRIMARY KEY,
189
+ turn_id VARCHAR NOT NULL,
190
+ model_id VARCHAR NOT NULL,
191
+ model_name VARCHAR,
192
+ input_tokens UINTEGER,
193
+ output_tokens UINTEGER,
194
+ cache_read_tokens UINTEGER,
195
+ cache_creation_tokens UINTEGER,
196
+ cost DECIMAL(10,6),
197
+ context_window INTEGER,
198
+ web_search_count INTEGER DEFAULT 0,
199
+ UNIQUE(turn_id, model_id)
200
+ );
201
+
202
+ CREATE INDEX IF NOT EXISTS idx_model_usage_turn ON turn_model_usage(turn_id);
203
+ CREATE INDEX IF NOT EXISTS idx_model_usage_model ON turn_model_usage(model_id);
204
+
205
+ -- Attachments (images, files) sent with prompts
206
+ CREATE TABLE IF NOT EXISTS turn_attachments (
207
+ id INTEGER PRIMARY KEY,
208
+ turn_id VARCHAR NOT NULL,
209
+ attachment_type VARCHAR NOT NULL,
210
+ file_path VARCHAR,
211
+ file_name VARCHAR,
212
+ media_type VARCHAR,
213
+ size_bytes INTEGER,
214
+ storage_path VARCHAR
215
+ );
216
+
217
+ CREATE INDEX IF NOT EXISTS idx_attachments_turn ON turn_attachments(turn_id);
218
+
219
+ -- Context snapshots (token usage from /context)
220
+ CREATE TABLE IF NOT EXISTS context_snapshots (
221
+ id INTEGER PRIMARY KEY,
222
+ session_id VARCHAR NOT NULL,
223
+ turn_id VARCHAR,
224
+ captured_at TIMESTAMPTZ DEFAULT now(),
225
+ trigger VARCHAR NOT NULL,
226
+ model VARCHAR,
227
+ total_tokens INTEGER,
228
+ context_window INTEGER,
229
+ usage_pct INTEGER,
230
+ system_prompt_tokens INTEGER,
231
+ conversation_tokens INTEGER,
232
+ files_tokens INTEGER,
233
+ tools_tokens INTEGER,
234
+ other_tokens INTEGER
235
+ );
236
+
237
+ CREATE INDEX IF NOT EXISTS idx_context_session ON context_snapshots(session_id);
238
+ CREATE INDEX IF NOT EXISTS idx_context_turn ON context_snapshots(turn_id);
239
+
240
+ -- Compaction events
241
+ CREATE TABLE IF NOT EXISTS compactions (
242
+ id INTEGER PRIMARY KEY,
243
+ session_id VARCHAR NOT NULL,
244
+ triggered_at TIMESTAMPTZ DEFAULT now(),
245
+ trigger VARCHAR,
246
+ trigger_turn_id VARCHAR,
247
+ pre_tokens INTEGER,
248
+ post_tokens INTEGER,
249
+ summary VARCHAR,
250
+ turns_compacted INTEGER
251
+ );
252
+
253
+ CREATE INDEX IF NOT EXISTS idx_compactions_session ON compactions(session_id);
254
+
255
+ -- Raw messages (optional, for deep FTS)
256
+ CREATE TABLE IF NOT EXISTS messages (
257
+ id INTEGER PRIMARY KEY,
258
+ session_id VARCHAR NOT NULL,
259
+ turn_id VARCHAR,
260
+ role VARCHAR NOT NULL,
261
+ content VARCHAR,
262
+ timestamp TIMESTAMPTZ
263
+ );
264
+
265
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
266
+ CREATE INDEX IF NOT EXISTS idx_messages_turn ON messages(turn_id);
267
+
268
+ -- Plans: first-class plan tracking
269
+ CREATE TABLE IF NOT EXISTS plans (
270
+ id VARCHAR PRIMARY KEY,
271
+ turn_id VARCHAR NOT NULL UNIQUE, -- The turn that created the plan
272
+ session_id VARCHAR NOT NULL,
273
+ project_hash VARCHAR NOT NULL,
274
+ git_repo_hash VARCHAR,
275
+
276
+ -- Plan identity
277
+ plan_file VARCHAR, -- e.g. ~/.claude/plans/zesty-prancing-hammock.md
278
+ plan_name VARCHAR, -- "zesty-prancing-hammock" (stem from path)
279
+
280
+ -- Content (short - full content read from file or shadow-git)
281
+ title VARCHAR, -- AI-generated: "Background Tasks Feature"
282
+ summary VARCHAR, -- AI-generated: 1-2 sentence description
283
+
284
+ -- Outcome tracking
285
+ status VARCHAR DEFAULT 'proposed', -- proposed | approved | rejected | abandoned
286
+ approved_at TIMESTAMPTZ,
287
+
288
+ -- Scope metrics (updated as implementation progresses)
289
+ implementation_turns INTEGER DEFAULT 0,
290
+ implementation_cost DECIMAL(10,4) DEFAULT 0,
291
+ files_changed INTEGER DEFAULT 0,
292
+
293
+ created_at TIMESTAMPTZ DEFAULT now(),
294
+ updated_at TIMESTAMPTZ DEFAULT now()
295
+ );
296
+
297
+ CREATE INDEX IF NOT EXISTS idx_plans_session ON plans(session_id);
298
+ CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_hash);
299
+ CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
300
+ CREATE INDEX IF NOT EXISTS idx_plans_repo ON plans(git_repo_hash);
301
+
302
+ -- Plan-turn links: which turns implemented a plan
303
+ CREATE TABLE IF NOT EXISTS plan_turns (
304
+ id INTEGER PRIMARY KEY,
305
+ plan_id VARCHAR NOT NULL,
306
+ turn_id VARCHAR NOT NULL,
307
+ role VARCHAR DEFAULT 'implementation', -- planning, implementation, verification
308
+ UNIQUE(plan_id, turn_id)
309
+ );
310
+
311
+ CREATE INDEX IF NOT EXISTS idx_plan_turns_plan ON plan_turns(plan_id);
312
+ CREATE INDEX IF NOT EXISTS idx_plan_turns_turn ON plan_turns(turn_id);
313
+
314
+ -- File review acceptances: per-file review status within a session
315
+ CREATE TABLE IF NOT EXISTS file_acceptances (
316
+ id VARCHAR PRIMARY KEY,
317
+ session_id VARCHAR NOT NULL,
318
+ project_hash VARCHAR NOT NULL,
319
+ file_path VARCHAR NOT NULL,
320
+ shadow_commit VARCHAR NOT NULL,
321
+ status VARCHAR DEFAULT 'reviewed', -- 'reviewed' | 'flagged'
322
+ note TEXT,
323
+ created_at TIMESTAMPTZ DEFAULT current_timestamp,
324
+ updated_at TIMESTAMPTZ DEFAULT current_timestamp
325
+ );
326
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_acceptances_session_file
327
+ ON file_acceptances(session_id, file_path);
328
+ CREATE INDEX IF NOT EXISTS idx_acceptances_session
329
+ ON file_acceptances(session_id);
330
+
331
+ -- Projects: one row per local working-copy path. id = SHA256(abspath).
332
+ -- merged_into is reserved for the future merge widget that will let users
333
+ -- manually link two projects (e.g. two clones of the same repo) — until
334
+ -- that ships, all rows have merged_into IS NULL.
335
+ CREATE TABLE IF NOT EXISTS projects (
336
+ id VARCHAR PRIMARY KEY,
337
+ name VARCHAR NOT NULL,
338
+ merged_into VARCHAR,
339
+ created_at TIMESTAMPTZ DEFAULT now()
340
+ );
341
+
342
+ CREATE INDEX IF NOT EXISTS idx_projects_merged ON projects(merged_into);
343
+
344
+ -- Sequences for auto-increment IDs
345
+ CREATE SEQUENCE IF NOT EXISTS seq_field_defs START 1;
346
+ CREATE SEQUENCE IF NOT EXISTS seq_turn_fields START 1;
347
+ CREATE SEQUENCE IF NOT EXISTS seq_tags START 1;
348
+ CREATE SEQUENCE IF NOT EXISTS seq_turn_files START 1;
349
+ CREATE SEQUENCE IF NOT EXISTS seq_turn_tools START 1;
350
+ CREATE SEQUENCE IF NOT EXISTS seq_model_usage START 1;
351
+ CREATE SEQUENCE IF NOT EXISTS seq_attachments START 1;
352
+ CREATE SEQUENCE IF NOT EXISTS seq_context START 1;
353
+ CREATE SEQUENCE IF NOT EXISTS seq_compactions START 1;
354
+ CREATE SEQUENCE IF NOT EXISTS seq_messages START 1;
355
+ CREATE SEQUENCE IF NOT EXISTS seq_plan_turns START 1;
356
+ CREATE SEQUENCE IF NOT EXISTS seq_file_acceptances START 1;
357
+ """
358
+
359
+
360
+ class _SchemaMixin:
361
+ """Schema init, migrations, FTS, and index recovery for ShadowDB."""
362
+
363
+ def _recover_indexes(self) -> duckdb.DuckDBPyConnection:
364
+ """Drop and recreate all indexes to fix ART corruption."""
365
+ logger.warning("Attempting ART index rebuild recovery...")
366
+ con = duckdb.connect(str(self.db_path))
367
+ # Extract CREATE INDEX statements from SCHEMA_SQL
368
+ index_stmts = []
369
+ for stmt in SCHEMA_SQL.split(";"):
370
+ s = stmt.strip()
371
+ if s.upper().startswith("CREATE INDEX"):
372
+ parts = s.split()
373
+ idx_name = parts[5] if "IF" in s.upper() else parts[2]
374
+ index_stmts.append((idx_name, s))
375
+ # Drop all indexes (safe — doesn't scan corrupt ART nodes)
376
+ for idx_name, _ in index_stmts:
377
+ try:
378
+ con.execute(f"DROP INDEX IF EXISTS {idx_name}")
379
+ except Exception:
380
+ pass
381
+ # Recreate from clean data
382
+ for idx_name, stmt in index_stmts:
383
+ try:
384
+ con.execute(stmt)
385
+ except Exception as e:
386
+ logger.warning(f"Index {idx_name} recreate skipped: {e}")
387
+ logger.warning(f"Shadow DB recovered via ART index rebuild ({len(index_stmts)} indexes)")
388
+ return con
389
+
390
+ def _init_schema(self):
391
+ """Initialize all tables and seed system field defs."""
392
+ con = self._con # Use raw connection (called from _get_con)
393
+ # Execute schema DDL (multiple statements)
394
+ for statement in SCHEMA_SQL.split(";"):
395
+ statement = statement.strip()
396
+ if statement:
397
+ try:
398
+ con.execute(statement)
399
+ except duckdb.FatalException:
400
+ raise # DB is dead, don't mask it
401
+ except Exception as e:
402
+ logger.warning(f"Schema statement skipped: {e}")
403
+
404
+ # ── Migrations ──
405
+ # NOTE: DuckDB has a bug where ALTER TABLE ADD COLUMN in the WAL
406
+ # fails on replay ("GetDefaultDatabase with no default database set").
407
+ # Every migration that uses ALTER TABLE MUST call _migrate_add_column()
408
+ # which checkpoints immediately after each ALTER.
409
+ self._migrate_add_column(con, "sessions", "git_repo_hash", "VARCHAR")
410
+ self._migrate_add_column(con, "turns", "git_repo_hash", "VARCHAR")
411
+ self._migrate_add_column(con, "turns", "is_plan", "BOOLEAN DEFAULT false")
412
+ # Multi-provider readiness: existing rows predate other CLIs → all Claude.
413
+ self._migrate_add_column(con, "sessions", "provider", "VARCHAR DEFAULT 'claude'")
414
+ self._migrate_add_column(con, "turns", "provider", "VARCHAR DEFAULT 'claude'")
415
+ # Provider-neutral rename: the summary-fork cost column was haiku_cost.
416
+ self._migrate_rename_column(con, "turns", "haiku_cost", "summary_cost")
417
+ # Provider-neutral rename of the provider conversation/turn identity columns
418
+ # (claude_session_id predates multi-provider). The index is renamed too.
419
+ self._migrate_rename_column(con, "turns", "claude_session_id", "provider_session_id")
420
+ self._migrate_rename_column(con, "turns", "claude_turn_uuid", "provider_turn_uuid")
421
+ self._migrate_rename_index(con, "idx_turns_claude_session", "idx_turns_provider_session", "turns", "provider_session_id")
422
+
423
+ # Backfill plans from existing turn_tools data
424
+ self._backfill_plans(con)
425
+
426
+ # Backfill git_repo_hash for existing rows from project path files
427
+ nulls = con.execute(
428
+ "SELECT COUNT(*) FROM turns WHERE git_repo_hash IS NULL AND status != 'interrupted'"
429
+ ).fetchone()[0]
430
+ if nulls > 0:
431
+ self._backfill_git_repo_hash(con)
432
+
433
+ # Seed system field definitions if empty
434
+ count = con.execute("SELECT COUNT(*) FROM field_defs WHERE source = 'system'").fetchone()[0]
435
+ if count == 0:
436
+ for fd in SYSTEM_FIELD_DEFS:
437
+ con.execute(
438
+ """INSERT INTO field_defs (id, project_hash, key, label, field_type, prompt, display_order, source)
439
+ VALUES (nextval('seq_field_defs'), NULL, ?, ?, ?, ?, ?, 'system')""",
440
+ [fd["key"], fd["label"], fd["field_type"], fd["prompt"], fd["display_order"]]
441
+ )
442
+ logger.info(f"Seeded {len(SYSTEM_FIELD_DEFS)} system field definitions")
443
+
444
+ # Repair desynced sequences (e.g. after rebuild tool used manual IDs)
445
+ self._repair_sequences(con)
446
+
447
+ # Mark stale pending turns from previous server runs
448
+ stale = con.execute(
449
+ "UPDATE turns SET status = 'interrupted' WHERE status = 'pending' RETURNING id"
450
+ ).fetchall()
451
+ if stale:
452
+ logger.info(f"Marked {len(stale)} stale pending turns as 'interrupted'")
453
+
454
+ # Full-text search setup
455
+ self._init_fts(con)
456
+
457
+ logger.info("Shadow DB schema initialized")
458
+
459
+ def _repair_sequences(self, con):
460
+ """Ensure DuckDB sequences are ahead of MAX(id) in their tables.
461
+
462
+ The rebuild tool uses manual ID counters, which can leave sequences
463
+ behind the actual max ID. This causes 'Duplicate key' errors on
464
+ subsequent nextval() calls.
465
+ """
466
+ seq_table_map = [
467
+ ("seq_turn_fields", "turn_fields"),
468
+ ("seq_tags", "tags"),
469
+ ("seq_turn_files", "turn_files"),
470
+ ("seq_turn_tools", "turn_tools"),
471
+ ("seq_model_usage", "turn_model_usage"),
472
+ ("seq_attachments", "turn_attachments"),
473
+ ("seq_context", "context_snapshots"),
474
+ ("seq_compactions", "compactions"),
475
+ ("seq_messages", "messages"),
476
+ ("seq_plan_turns", "plan_turns"),
477
+ ]
478
+ # Read current sequence values from catalog (no side effects, unlike nextval)
479
+ try:
480
+ rows = con.execute(
481
+ "SELECT sequence_name, start_value FROM duckdb_sequences()"
482
+ ).fetchall()
483
+ seq_values = {r[0]: r[1] for r in rows}
484
+ except Exception as e:
485
+ logger.warning(f"Cannot read duckdb_sequences(), skipping repair: {e}")
486
+ return
487
+
488
+ repaired = []
489
+ for seq_name, table_name in seq_table_map:
490
+ try:
491
+ max_id = con.execute(f"SELECT COALESCE(MAX(id), 0) FROM {table_name}").fetchone()[0]
492
+ if max_id == 0:
493
+ continue
494
+ next_val = seq_values.get(seq_name)
495
+ if next_val is not None and next_val <= max_id:
496
+ target = max_id + 1
497
+ con.execute(f"DROP SEQUENCE {seq_name}")
498
+ con.execute(f"CREATE SEQUENCE {seq_name} START {target}")
499
+ repaired.append(f"{seq_name}: {next_val}→{target}")
500
+ except Exception as e:
501
+ logger.warning(f"Sequence repair skipped for {seq_name}: {e}")
502
+ if repaired:
503
+ logger.warning(f"Repaired desynced sequences: {', '.join(repaired)}")
504
+
505
+ def _init_fts(self, con):
506
+ """Load FTS extension and build indexes."""
507
+ try:
508
+ con.execute("INSTALL fts; LOAD fts;")
509
+ self._rebuild_fts_indexes(con)
510
+ except Exception as e:
511
+ logger.warning(f"FTS init failed (search will use ILIKE fallback): {e}")
512
+ self._fts_ready = False
513
+
514
+ def _rebuild_fts_indexes(self, con):
515
+ """Build/rebuild FTS indexes on turns.user_prompt and turn_fields.search_text."""
516
+ t0 = time.monotonic()
517
+ try:
518
+ con.execute(
519
+ "PRAGMA create_fts_index('turns', 'id', 'user_prompt', "
520
+ "stemmer='porter', stopwords='english', overwrite=1)"
521
+ )
522
+ con.execute(
523
+ "PRAGMA create_fts_index('turn_fields', 'id', 'search_text', "
524
+ "stemmer='porter', stopwords='english', overwrite=1)"
525
+ )
526
+ self._fts_ready = True
527
+ self._fts_dirty = False
528
+ elapsed = (time.monotonic() - t0) * 1000
529
+ logger.info(f"FTS indexes built ({elapsed:.0f}ms)")
530
+ except Exception as e:
531
+ logger.warning(f"FTS index build failed: {e}")
532
+ self._fts_ready = False
533
+
534
+ def rebuild_fts_if_needed(self):
535
+ """Rebuild FTS indexes if data has changed since last build."""
536
+ if not self._fts_dirty:
537
+ return
538
+ with self._lock:
539
+ if not self._fts_dirty:
540
+ return
541
+ con = self._get_con()
542
+ try:
543
+ con.execute("LOAD fts;")
544
+ except Exception:
545
+ pass
546
+ self._rebuild_fts_indexes(con)
547
+
548
+ @staticmethod
549
+ def _migrate_add_column(con, table: str, column: str, col_type: str):
550
+ """Add a column if it doesn't exist, with immediate checkpoint.
551
+
552
+ DuckDB WAL replay bug: ALTER TABLE in WAL fails on restart.
553
+ Checkpoint flushes the ALTER into the DB file so WAL never replays it.
554
+ """
555
+ try:
556
+ con.execute(f"SELECT {column} FROM {table} LIMIT 0")
557
+ except duckdb.FatalException:
558
+ raise # DB is dead
559
+ except Exception:
560
+ con.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
561
+ con.execute("CHECKPOINT")
562
+ logger.info(f"Migration: added {table}.{column} ({col_type}) + checkpoint")
563
+
564
+ @staticmethod
565
+ def _migrate_rename_column(con, table: str, old: str, new: str):
566
+ """Rename a column if the old name still exists, with immediate checkpoint.
567
+
568
+ Idempotent: skips when the new column already exists (fresh schema or
569
+ already migrated). CHECKPOINT after the ALTER works around DuckDB's
570
+ WAL-replay bug (the rename must not live only in the WAL on restart).
571
+
572
+ In DuckDB an FTS index AND the regular ART indexes on the table are
573
+ catalog dependencies that block RENAME COLUMN ("Cannot alter entry ...
574
+ because there are entries that depend on it"). So we capture each ART
575
+ index's CREATE SQL, drop them (and the FTS index), rename, then rebuild
576
+ the ART indexes from their saved SQL. _init_fts() rebuilds the FTS index
577
+ later in this same schema-init pass.
578
+ """
579
+ try:
580
+ con.execute(f"SELECT {new} FROM {table} LIMIT 0")
581
+ return # new column already present — nothing to do
582
+ except duckdb.FatalException:
583
+ raise # DB is dead
584
+ except Exception:
585
+ pass
586
+ try:
587
+ con.execute(f"SELECT {old} FROM {table} LIMIT 0")
588
+ except duckdb.FatalException:
589
+ raise
590
+ except Exception:
591
+ return # neither column exists — let the CREATE TABLE DDL own it
592
+
593
+ # Capture ART index definitions so we can rebuild them after the rename.
594
+ index_sql = []
595
+ try:
596
+ for name, sql in con.execute(
597
+ "SELECT index_name, sql FROM duckdb_indexes() WHERE table_name = ?", [table]
598
+ ).fetchall():
599
+ if sql:
600
+ index_sql.append((name, sql))
601
+ except Exception as e:
602
+ logger.warning(f"Could not enumerate indexes on {table}: {e}")
603
+
604
+ # Drop the FTS index (separate dependency) and every ART index.
605
+ try:
606
+ con.execute("LOAD fts;")
607
+ con.execute(f"PRAGMA drop_fts_index('{table}')")
608
+ except Exception:
609
+ pass # no FTS index on this table (or extension absent) — fine
610
+ for name, _ in index_sql:
611
+ try:
612
+ con.execute(f"DROP INDEX IF EXISTS {name}")
613
+ except Exception as e:
614
+ logger.warning(f"Could not drop index {name}: {e}")
615
+
616
+ con.execute(f"ALTER TABLE {table} RENAME COLUMN {old} TO {new}")
617
+ con.execute("CHECKPOINT")
618
+
619
+ # Rebuild the ART indexes, rewriting any reference to the renamed column
620
+ # in the captured DDL (e.g. an index defined ON the column we just renamed).
621
+ for name, sql in index_sql:
622
+ try:
623
+ con.execute(sql.replace(old, new))
624
+ except Exception as e:
625
+ logger.warning(f"Could not recreate index {name}: {e}")
626
+ con.execute("CHECKPOINT")
627
+ logger.info(
628
+ f"Migration: renamed {table}.{old} → {new} "
629
+ f"(dropped/rebuilt {len(index_sql)} ART indexes + FTS) + checkpoint"
630
+ )
631
+
632
+ @staticmethod
633
+ def _migrate_rename_index(con, old_name: str, new_name: str, table: str, columns: str):
634
+ """Rename an index (DuckDB has no RENAME INDEX): drop the old, create the
635
+ new. Idempotent + checkpointed. Used when a renamed column's index should
636
+ also shed the old name."""
637
+ try:
638
+ con.execute(f"DROP INDEX IF EXISTS {old_name}")
639
+ con.execute(f"CREATE INDEX IF NOT EXISTS {new_name} ON {table}({columns})")
640
+ con.execute("CHECKPOINT")
641
+ except duckdb.FatalException:
642
+ raise
643
+ except Exception as e:
644
+ logger.warning(f"Index rename {old_name} → {new_name} skipped: {e}")
645
+
646
+ def _backfill_git_repo_hash(self, con):
647
+ """One-time backfill: compute git_repo_hash for existing rows."""
648
+ try:
649
+ from painapple_code.paths import get_git_repo_hash
650
+
651
+ # Build mapping from project_hash -> git_repo_hash via path files
652
+ projects_dir = paths.DATA_HOME / "projects"
653
+ if not projects_dir.exists():
654
+ return
655
+
656
+ mapping = {}
657
+ for h_dir in projects_dir.iterdir():
658
+ if h_dir.is_dir():
659
+ path_file = h_dir / "path"
660
+ if path_file.exists():
661
+ project_path = path_file.read_text(encoding="utf-8").strip()
662
+ repo_hash = get_git_repo_hash(project_path)
663
+ if repo_hash:
664
+ mapping[h_dir.name] = repo_hash
665
+
666
+ for ph, rh in mapping.items():
667
+ for table in ("turns", "sessions"):
668
+ rows = con.execute(
669
+ f"UPDATE {table} SET git_repo_hash = ? "
670
+ f"WHERE project_hash = ? AND git_repo_hash IS NULL RETURNING id",
671
+ [rh, ph]
672
+ ).fetchall()
673
+ if rows:
674
+ logger.info(f"Backfill: set git_repo_hash={rh} on {len(rows)} {table} rows (project={ph})")
675
+ except Exception as e:
676
+ logger.warning(f"git_repo_hash backfill failed (non-fatal): {e}")