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,915 @@
1
+ /**
2
+ * ThinkingController - Handles rendering of Claude's thinking blocks
3
+ *
4
+ * DESIGN (2025-12-29):
5
+ * - Each thinking step shown inline with its tools
6
+ * - Reasoning text is collapsed (one-line preview), expandable on click
7
+ * - Tools are ALWAYS visible right after their reasoning step
8
+ * - Preserves chronological order: thought1 + tools1, thought2 + tools2, etc.
9
+ */
10
+
11
+ import { $, escapeHtml, escapeAttr, parseTaskUsage, formatTokensBadge, formatDuration } from '../utils.js';
12
+ import { getToolCollapseMode, getToolCategory } from '../widgets/config-widget.js';
13
+
14
+ export class ThinkingController {
15
+ constructor(ctx) {
16
+ this.ctx = ctx;
17
+ }
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // THINKING MESSAGE RENDERING
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ /**
24
+ * Render a thinking message with automatic grouping
25
+ * Each thinking message becomes a step with its tools
26
+ */
27
+ renderThinkingMessageWithGrouping(msg) {
28
+ const messages = this.ctx.getMessageContainer();
29
+ const lastChild = messages.lastElementChild;
30
+
31
+ // If last element is a thinking section from the SAME turn, add to it
32
+ // (never group across turn boundaries - see commit 06a62c8 for precedent)
33
+ if (lastChild?.classList.contains('thinking') &&
34
+ lastChild.querySelector('.thinking-section') &&
35
+ lastChild.dataset.turnId == msg.turnId) {
36
+ this.addStepToSection(lastChild, msg);
37
+ return;
38
+ }
39
+
40
+ // Otherwise create new thinking section
41
+ const div = document.createElement('div');
42
+ div.className = 'message thinking';
43
+ div.id = `msg-${msg.id}`;
44
+ div.dataset.turnId = msg.turnId;
45
+ div.innerHTML = this._renderSection([msg]);
46
+ messages.appendChild(div);
47
+
48
+ // Check for gutter space after DOM is updated
49
+ requestAnimationFrame(() => {
50
+ const section = div.querySelector('.thinking-section');
51
+ if (section) this.checkGutterSpace(section);
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Add a new step to an existing thinking section
57
+ */
58
+ addStepToSection(sectionElement, msg) {
59
+ const section = sectionElement.querySelector('.thinking-section');
60
+ if (!section) return;
61
+
62
+ // Prevent duplicate steps
63
+ if (section.querySelector(`.thinking-step[data-msg-id="${msg.id}"]`)) {
64
+ return;
65
+ }
66
+
67
+ const existingSteps = section.querySelectorAll('.thinking-step');
68
+ const stepNum = existingSteps.length + 1;
69
+ const sectionId = section.id;
70
+
71
+ const stepHtml = this._renderStep(msg, stepNum, sectionId);
72
+ section.insertAdjacentHTML('beforeend', stepHtml);
73
+
74
+ // Check gutter space (may not have been checked if this is first tool)
75
+ requestAnimationFrame(() => this.checkGutterSpace(section));
76
+ }
77
+
78
+ /**
79
+ * Render a complete thinking section
80
+ */
81
+ _renderSection(thinkingMsgs) {
82
+ const sectionId = `thinking-section-${thinkingMsgs[0].id}`;
83
+
84
+ const stepsHtml = thinkingMsgs.map((msg, idx) => {
85
+ return this._renderStep(msg, idx + 1, sectionId);
86
+ }).join('');
87
+
88
+ // Only show collapse button if there are any tools
89
+ // Button at START of section so sticky-top works when scrolling down
90
+ // Double chevrons indicate "collapse ALL" vs single chevron for individual tools
91
+ const hasTools = thinkingMsgs.some(msg => msg.tools && msg.tools.length > 0);
92
+ const collapseBtn = hasTools ? `
93
+ <button class="thinking-collapse-tools-btn" data-act="toggle-thinking-tools" data-id="${escapeAttr(sectionId)}" data-tooltip="Collapse/expand all tool outputs">
94
+ <svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
95
+ <path d="M7 13l5 5 5-5M7 6l5 5 5-5"/>
96
+ </svg>
97
+ <svg class="expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
98
+ <path d="M13 7l5 5-5 5M6 7l5 5-5 5"/>
99
+ </svg>
100
+ </button>
101
+ ` : '';
102
+
103
+ return `<div class="thinking-section" id="${sectionId}">${collapseBtn}${stepsHtml}</div>`;
104
+ }
105
+
106
+ /**
107
+ * Render a single step: collapsed reasoning + visible tools
108
+ * Text is truncated by default, shows full markdown when expanded
109
+ */
110
+ _renderStep(msg, stepNum, sectionId) {
111
+ const content = (msg.content || '').trim();
112
+ const hasContent = content.length > 0;
113
+
114
+ // Render tools for this step (always visible)
115
+ const toolsHtml = this._renderStepTools(msg.tools || []);
116
+
117
+ // Preview text (plain, truncated, markdown stripped) - shown when collapsed
118
+ const strippedContent = this._stripMarkdownSyntax(content);
119
+ const previewText = strippedContent.slice(0, 200);
120
+ const previewHtml = hasContent
121
+ ? `<span class="thinking-step-preview">${escapeHtml(previewText)}${strippedContent.length > 200 ? '...' : ''}</span>`
122
+ : '';
123
+
124
+ // Full content with markdown - shown when expanded
125
+ const fullContentHtml = hasContent
126
+ ? `<div class="thinking-step-content">${this._renderMarkdown(content)}</div>`
127
+ : '';
128
+
129
+ // The API hands us signature-only thinking blocks (thinking: "") often
130
+ // enough that a permanently-empty clickable bar is a real artifact, so
131
+ // drop the header when there is nothing to preview or expand. The step
132
+ // itself always stays — it is the DOM anchor updateThinkingMessageTool()
133
+ // looks up by data-msg-id when tools stream in afterwards. CSS keeps a
134
+ // header-less, tool-less step hidden until a tool card actually lands.
135
+ const headerHtml = hasContent
136
+ ? `<div class="thinking-step-header" data-act="toggle-thinking-step" data-id="${escapeAttr(sectionId)}" data-step="${stepNum}">
137
+ ${previewHtml}
138
+ <span class="thinking-step-expand">›</span>
139
+ </div>`
140
+ : '';
141
+
142
+ return `
143
+ <div class="thinking-step" data-step="${stepNum}" data-msg-id="${msg.id}">
144
+ ${headerHtml}
145
+ ${fullContentHtml}
146
+ <div class="thinking-step-tools">
147
+ ${toolsHtml}
148
+ </div>
149
+ </div>
150
+ `;
151
+ }
152
+
153
+ /**
154
+ * Render markdown content using the app's markdown renderer
155
+ */
156
+ _renderMarkdown(content) {
157
+ if (!content) return '';
158
+ if (this.ctx.markdown) {
159
+ return this.ctx.markdown.render(content);
160
+ }
161
+ // Fallback: escape HTML and preserve whitespace
162
+ return `<pre style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
163
+ }
164
+
165
+ /**
166
+ * Strip markdown syntax for clean preview text
167
+ * Safe approach: just removes syntax markers, doesn't render
168
+ */
169
+ _stripMarkdownSyntax(text) {
170
+ if (!text) return '';
171
+ return text
172
+ // Remove heading markers (# ## ### etc)
173
+ .replace(/^#{1,6}\s+/gm, '')
174
+ // Remove bold/italic markers
175
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
176
+ .replace(/\*([^*]+)\*/g, '$1')
177
+ .replace(/__([^_]+)__/g, '$1')
178
+ // Only match _italic_ at word boundaries (not mid-word like ai_overview)
179
+ .replace(/(^|\s)_([^\s_]+)_(\s|$)/g, '$1$2$3')
180
+ // Remove inline code backticks
181
+ .replace(/`([^`]+)`/g, '$1')
182
+ // Remove blockquote markers
183
+ .replace(/^>\s*/gm, '')
184
+ // Remove list markers
185
+ .replace(/^[-*+]\s+/gm, '')
186
+ .replace(/^\d+\.\s+/gm, '')
187
+ // Remove link syntax [text](url) -> text
188
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
189
+ // Collapse multiple spaces/newlines into single space
190
+ .replace(/\s+/g, ' ')
191
+ .trim();
192
+ }
193
+
194
+ /**
195
+ * Render tools for a step (always visible)
196
+ * Groups sub-agent tools under their parent Task tool
197
+ */
198
+ _renderStepTools(tools) {
199
+ if (!tools || tools.length === 0) return '';
200
+
201
+ // Group tools: Tasks capture subsequent tools until completed
202
+ const groups = this._groupToolsByTask(tools);
203
+
204
+ return groups.map(group => {
205
+ if (group.type === 'task') {
206
+ return this._renderTaskGroup(group);
207
+ } else {
208
+ return this._renderSingleTool(group.tool);
209
+ }
210
+ }).filter(Boolean).join('');
211
+ }
212
+
213
+ /**
214
+ * Group tools by Task ownership
215
+ * Task tools capture all subsequent tools until we hit another Task tool
216
+ * (regardless of completion status - completion only affects the badge)
217
+ */
218
+ _groupToolsByTask(tools) {
219
+ const groups = [];
220
+ let currentTaskGroup = null;
221
+
222
+ for (const tool of tools) {
223
+ if (tool.toolName === 'Task') {
224
+ // Close any previous task group
225
+ if (currentTaskGroup) {
226
+ groups.push(currentTaskGroup);
227
+ }
228
+ // Start new task group
229
+ currentTaskGroup = {
230
+ type: 'task',
231
+ task: tool,
232
+ children: []
233
+ };
234
+ } else if (currentTaskGroup) {
235
+ // Add to current task group (all tools after Task belong to it)
236
+ currentTaskGroup.children.push(tool);
237
+ } else {
238
+ // Independent tool (before any Task)
239
+ groups.push({ type: 'tool', tool });
240
+ }
241
+ }
242
+
243
+ // Don't forget the last group
244
+ if (currentTaskGroup) {
245
+ groups.push(currentTaskGroup);
246
+ }
247
+
248
+ return groups;
249
+ }
250
+
251
+ /**
252
+ * Render a single tool (non-Task)
253
+ */
254
+ _renderSingleTool(tool) {
255
+ const toolType = getToolCategory(tool.toolName);
256
+ const mode = getToolCollapseMode('thinking', toolType);
257
+ const expandOpts = { defaultExpanded: mode === 'expanded' };
258
+ const msg = {
259
+ id: tool.toolId,
260
+ toolInput: tool.toolInput,
261
+ toolOutput: tool.toolOutput,
262
+ toolError: tool.toolError,
263
+ toolCompleted: tool.toolCompleted || !!tool.toolOutput
264
+ };
265
+
266
+ let html = '';
267
+ // Read/Write/Edit need file_path. If missing (truncated input, partial
268
+ // streaming state, malformed log), fall through to the compact renderer
269
+ // — its renderThinkingToolCompact path already handles missing inputs.
270
+ const hasFilePath = !!tool.toolInput?.file_path;
271
+ switch (tool.toolName) {
272
+ case 'Edit':
273
+ html = hasFilePath
274
+ ? this.ctx.toolRenderer.renderEditDiff(tool.toolInput, tool.toolOutput, tool.toolId, tool.startLine)
275
+ : this.ctx.toolRenderer.renderThinkingToolCompact(tool);
276
+ break;
277
+ case 'Write':
278
+ html = hasFilePath
279
+ ? this.ctx.toolRenderer.renderWriteBlock(msg, expandOpts)
280
+ : this.ctx.toolRenderer.renderThinkingToolCompact(tool);
281
+ break;
282
+ case 'Read':
283
+ html = hasFilePath
284
+ ? this.ctx.toolRenderer.renderReadBlock(msg, expandOpts)
285
+ : this.ctx.toolRenderer.renderThinkingToolCompact(tool);
286
+ break;
287
+ case 'Grep':
288
+ html = this.ctx.toolRenderer.renderGrepBlock(msg, expandOpts);
289
+ break;
290
+ case 'Glob':
291
+ html = this.ctx.toolRenderer.renderGlobBlock(msg, expandOpts);
292
+ break;
293
+ case 'Bash':
294
+ case 'Shell':
295
+ html = this.ctx.toolRenderer.renderBashBlock(msg, expandOpts);
296
+ break;
297
+ case 'TodoWrite':
298
+ msg.toolName = 'TodoWrite';
299
+ html = this.ctx.toolRenderer.renderTodoBlock(msg);
300
+ break;
301
+ default:
302
+ html = this.ctx.toolRenderer.renderThinkingToolCompact(tool);
303
+ }
304
+
305
+ if (!html) return '';
306
+
307
+ // Gutter icon for per-tool collapse (positioned in right margin via CSS)
308
+ const gutterIcon = `
309
+ <button class="tool-gutter-icon" data-act="toggle-tool-collapse" data-id="${escapeAttr(tool.toolId)}" data-tooltip="Collapse/expand this tool">
310
+ <svg class="gutter-collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
311
+ <path d="M19 9l-7 7-7-7"/>
312
+ </svg>
313
+ <svg class="gutter-expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
314
+ <path d="M9 5l7 7-7 7"/>
315
+ </svg>
316
+ </button>
317
+ `;
318
+
319
+ const collapsedClass = mode === 'collapsed' ? ' tool-collapsed' : '';
320
+ return `<div class="thinking-tool-card${collapsedClass}" data-tool-id="${tool.toolId}">${gutterIcon}${html}</div>`;
321
+ }
322
+
323
+ /**
324
+ * Render a Task group with collapsible children
325
+ */
326
+ _renderTaskGroup(group) {
327
+ const { task, children } = group;
328
+ const isComplete = task.toolCompleted || !!task.toolOutput;
329
+ const desc = task.toolInput?.description || 'Running task...';
330
+ const subagentType = task.toolInput?.subagent_type || '';
331
+
332
+ // Status badge
333
+ const statusBadge = isComplete
334
+ ? '<span class="task-group-badge task-group-badge-success">Done</span>'
335
+ : '<span class="task-group-badge task-group-badge-pending">Running...</span>';
336
+
337
+ // Count badge for children
338
+ const childCount = children.length;
339
+ const countBadge = childCount > 0
340
+ ? `<span class="task-group-count">${childCount} tool${childCount !== 1 ? 's' : ''}</span>`
341
+ : '';
342
+
343
+ // Render children (collapsed by default)
344
+ const childrenHtml = children.length > 0
345
+ ? children.map(child => this._renderSingleTool(child)).join('')
346
+ : '';
347
+
348
+ // Parse agent response from toolOutput
349
+ const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(task.toolOutput);
350
+
351
+ // Usage badges (tokens + duration)
352
+ let usageBadges = '';
353
+ if (usage) {
354
+ const tokBadge = formatTokensBadge(usage.totalTokens);
355
+ const durBadge = formatDuration(usage.durationMs);
356
+ if (tokBadge) usageBadges += `<span class="task-group-tokens">${tokBadge}</span>`;
357
+ if (durBadge) usageBadges += `<span class="task-group-duration">${durBadge}</span>`;
358
+ }
359
+
360
+ // Preview for collapsed state (first ~150 chars, markdown stripped)
361
+ let outputPreview = '';
362
+ if (isComplete && agentResponse) {
363
+ const strippedResponse = this._stripMarkdownSyntax(agentResponse);
364
+ const preview = strippedResponse.slice(0, 150).trim();
365
+ if (preview) {
366
+ outputPreview = `<div class="task-group-preview">${escapeHtml(preview)}${strippedResponse.length > 150 ? '...' : ''}</div>`;
367
+ }
368
+ }
369
+
370
+ // Full response for expanded state (rendered with markdown)
371
+ let fullResponse = '';
372
+ if (isComplete && agentResponse) {
373
+ fullResponse = `<div class="task-group-response">${this._renderMarkdown(agentResponse)}</div>`;
374
+ }
375
+
376
+ // Show expand chevron if there are children OR a response to show
377
+ const hasExpandableContent = childCount > 0 || agentResponse;
378
+
379
+ return `
380
+ <div class="task-group" data-task-id="${escapeAttr(task.toolId)}" data-complete="${isComplete}">
381
+ <div class="task-group-header" data-act="toggle-task-group" data-id="${escapeAttr(task.toolId)}">
382
+ <span class="task-group-icon">
383
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
384
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
385
+ <circle cx="9" cy="7" r="4"/>
386
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
387
+ <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
388
+ </svg>
389
+ </span>
390
+ <span class="task-group-label">Task</span>
391
+ <span class="task-group-desc">${escapeHtml(desc)}</span>
392
+ ${subagentType ? `<span class="task-group-type">${escapeHtml(subagentType)}</span>` : ''}
393
+ ${countBadge}
394
+ ${usageBadges}
395
+ ${statusBadge}
396
+ ${hasExpandableContent ? '<span class="task-group-expand">›</span>' : ''}
397
+ </div>
398
+ ${outputPreview}
399
+ <div class="task-group-body">
400
+ ${fullResponse}
401
+ ${childrenHtml ? `<div class="task-group-children">${childrenHtml}</div>` : ''}
402
+ </div>
403
+ </div>
404
+ `;
405
+ }
406
+
407
+ /**
408
+ * Parse Task toolOutput to extract text content
409
+ * Output format is usually: [{"type":"text","text":"..."}]
410
+ */
411
+ _parseTaskOutputWithUsage(toolOutput) {
412
+ const raw = this._parseTaskOutput(toolOutput);
413
+ return parseTaskUsage(raw);
414
+ }
415
+
416
+ _parseTaskOutput(toolOutput) {
417
+ if (!toolOutput) return '';
418
+
419
+ try {
420
+ // If it's a string that looks like JSON array, parse it
421
+ let parsed = toolOutput;
422
+ if (typeof toolOutput === 'string') {
423
+ if (toolOutput.startsWith('[')) {
424
+ parsed = JSON.parse(toolOutput);
425
+ } else {
426
+ // Plain string, return as-is
427
+ return toolOutput;
428
+ }
429
+ }
430
+
431
+ // Extract text from content blocks
432
+ if (Array.isArray(parsed)) {
433
+ return parsed
434
+ .filter(block => block.type === 'text' && block.text)
435
+ .map(block => block.text)
436
+ .join('\n\n');
437
+ }
438
+
439
+ // Object with text property
440
+ if (parsed && typeof parsed === 'object' && parsed.text) {
441
+ return parsed.text;
442
+ }
443
+
444
+ // Fallback: stringify if we couldn't parse
445
+ return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
446
+ } catch (e) {
447
+ // JSON parse failed, return as-is
448
+ return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
449
+ }
450
+ }
451
+
452
+ // ═══════════════════════════════════════════════════════════════
453
+ // TOGGLE FUNCTIONS
454
+ // ═══════════════════════════════════════════════════════════════
455
+
456
+ toggleThinkingStep(sectionId, stepNum) {
457
+ const section = $(`#${sectionId}`);
458
+ if (!section) return;
459
+
460
+ const step = section.querySelector(`.thinking-step[data-step="${stepNum}"]`);
461
+ if (step) {
462
+ step.classList.toggle('expanded');
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Toggle all tool outputs in a thinking section (collapse/expand)
468
+ * Also syncs individual tool card states so their icons update correctly
469
+ */
470
+ toggleThinkingTools(sectionId) {
471
+ const section = $(`#${sectionId}`);
472
+ if (!section) return;
473
+
474
+ const isCollapsing = !section.classList.contains('tools-collapsed');
475
+ section.classList.toggle('tools-collapsed');
476
+
477
+ // Sync all individual tool cards to match the section state
478
+ // This ensures per-tool icons show correct expand/collapse state
479
+ const toolCards = section.querySelectorAll('.thinking-tool-card');
480
+ toolCards.forEach(card => {
481
+ if (isCollapsing) {
482
+ card.classList.add('tool-collapsed');
483
+ } else {
484
+ card.classList.remove('tool-collapsed');
485
+ }
486
+ });
487
+
488
+ // After collapsing, scroll to bring the section into view
489
+ // But only if it's not already visible (avoid jarring jumps)
490
+ if (isCollapsing) {
491
+ requestAnimationFrame(() => {
492
+ const message = section.closest('.message');
493
+ if (message) {
494
+ const container = document.getElementById('messages-container');
495
+ if (container) {
496
+ const msgRect = message.getBoundingClientRect();
497
+ const containerRect = container.getBoundingClientRect();
498
+ // Only scroll if message top is above visible area
499
+ if (msgRect.top < containerRect.top) {
500
+ message.scrollIntoView({ behavior: 'smooth', block: 'start' });
501
+ }
502
+ }
503
+ }
504
+ });
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Toggle a single tool's collapse state
510
+ */
511
+ toggleToolCollapse(toolId) {
512
+ const toolCard = document.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
513
+ if (toolCard) {
514
+ toolCard.classList.toggle('tool-collapsed');
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Check if there's margin space for gutter icons
520
+ * Called after render and on resize
521
+ */
522
+ checkGutterSpace(section) {
523
+ const messageEl = section.closest('.message');
524
+ if (!messageEl) return;
525
+
526
+ // Use the scrolling container, not the inner messages div
527
+ // #messages-container has padding and may have extra space on the right
528
+ const container = document.getElementById('messages-container');
529
+ if (!container) return;
530
+
531
+ const messageRect = messageEl.getBoundingClientRect();
532
+ const containerRect = container.getBoundingClientRect();
533
+
534
+ // Right space: distance from message edge to container edge
535
+ // Subtract ~20px for scrollbar buffer
536
+ const rightSpace = containerRect.right - messageRect.right - 20;
537
+
538
+ if (rightSpace >= 40) {
539
+ section.classList.add('has-gutter-space');
540
+ } else {
541
+ section.classList.remove('has-gutter-space');
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Check gutter space for all visible thinking sections
547
+ * Called on window resize
548
+ */
549
+ checkAllGutterSpace() {
550
+ document.querySelectorAll('.thinking-section').forEach(section => {
551
+ this.checkGutterSpace(section);
552
+ });
553
+ }
554
+
555
+ // Legacy compatibility
556
+ toggleThinking(id) {
557
+ const step = document.querySelector(`.thinking-step[data-msg-id="${id}"]`);
558
+ if (step) step.classList.toggle('expanded');
559
+ }
560
+
561
+ toggleThinkingGroup(groupId) {
562
+ // No-op - groups are always visible now
563
+ }
564
+
565
+ toggleThinkingThoughts(sectionId) {
566
+ // No-op - thoughts are always visible now
567
+ }
568
+
569
+ toggleThought(thoughtId) {
570
+ const step = document.querySelector(`.thinking-step[data-msg-id="${thoughtId}"]`);
571
+ if (step) step.classList.toggle('expanded');
572
+ }
573
+
574
+ // ═══════════════════════════════════════════════════════════════
575
+ // LIVE UPDATES (tool results streaming in)
576
+ // ═══════════════════════════════════════════════════════════════
577
+
578
+ /**
579
+ * Add a tool to a thinking message (live update)
580
+ * @param {Object} msg - The thinking message
581
+ * @param {Object} block - The tool_use block
582
+ * @param {string|null} parentTaskId - Server-provided parent Task ID for nesting
583
+ */
584
+ updateThinkingMessageTool(msg, block, parentTaskId = null) {
585
+ const step = document.querySelector(`.thinking-step[data-msg-id="${msg.id}"]`);
586
+ if (!step) return;
587
+
588
+ const toolsContainer = step.querySelector('.thinking-step-tools');
589
+ if (!toolsContainer) return;
590
+
591
+ // Check if tool already exists
592
+ if (toolsContainer.querySelector(`[data-tool-id="${block.id}"]`)) return;
593
+
594
+ const tool = {
595
+ toolName: block.name,
596
+ toolInput: block.input,
597
+ toolId: block.id,
598
+ toolCompleted: false,
599
+ toolOutput: null,
600
+ parentTaskId
601
+ };
602
+
603
+ if (block.name === 'Task') {
604
+ // Render new Task group
605
+ const taskHtml = this._renderTaskGroup({
606
+ task: tool,
607
+ children: []
608
+ });
609
+ toolsContainer.insertAdjacentHTML('beforeend', taskHtml);
610
+ } else if (parentTaskId) {
611
+ // Server told us the parent - nest inside that Task group
612
+ this._nestToolInTaskGroup(toolsContainer, parentTaskId, tool);
613
+ } else {
614
+ // No parent info - check for single active Task (fallback heuristic)
615
+ const incompleteTasks = toolsContainer.querySelectorAll(
616
+ '.task-group:not([data-complete="true"])'
617
+ );
618
+
619
+ if (incompleteTasks.length === 1) {
620
+ // Single active Task - safe to nest
621
+ this._nestToolInTaskGroup(
622
+ toolsContainer,
623
+ incompleteTasks[0].dataset.taskId,
624
+ tool
625
+ );
626
+ } else {
627
+ // Multiple or no Tasks - render flat (safe fallback)
628
+ const toolHtml = this._renderSingleTool(tool);
629
+ if (toolHtml) {
630
+ toolsContainer.insertAdjacentHTML('beforeend', toolHtml);
631
+ }
632
+ }
633
+ }
634
+
635
+ // Ensure collapse button exists now that we have a tool
636
+ this._ensureCollapseButton(step);
637
+ }
638
+
639
+ /**
640
+ * Ensure the collapse button exists in the thinking section
641
+ * Called when tools are added live (after initial render)
642
+ */
643
+ _ensureCollapseButton(step) {
644
+ const section = step.closest('.thinking-section');
645
+ if (!section) return;
646
+
647
+ // Check for gutter space (may have tools now)
648
+ this.checkGutterSpace(section);
649
+
650
+ // Already has button
651
+ if (section.querySelector('.thinking-collapse-tools-btn')) return;
652
+
653
+ // Insert at START for sticky-top to work when scrolling down
654
+ // Double chevrons indicate "collapse ALL"
655
+ const sectionId = section.id;
656
+ const btnHtml = `
657
+ <button class="thinking-collapse-tools-btn" data-act="toggle-thinking-tools" data-id="${escapeAttr(sectionId)}" data-tooltip="Collapse/expand all tool outputs">
658
+ <svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
659
+ <path d="M7 13l5 5 5-5M7 6l5 5 5-5"/>
660
+ </svg>
661
+ <svg class="expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14">
662
+ <path d="M13 7l5 5-5 5M6 7l5 5-5 5"/>
663
+ </svg>
664
+ </button>
665
+ `;
666
+ section.insertAdjacentHTML('afterbegin', btnHtml);
667
+ }
668
+
669
+ /**
670
+ * Nest a tool inside a Task group
671
+ */
672
+ _nestToolInTaskGroup(container, taskId, tool) {
673
+ const taskGroup = container.querySelector(`.task-group[data-task-id="${taskId}"]`);
674
+ if (!taskGroup) {
675
+ // Task group not found - render flat as fallback
676
+ console.warn(`[ThinkingController] Task group not found for ${taskId}, rendering flat`);
677
+ const toolHtml = this._renderSingleTool(tool);
678
+ if (toolHtml) {
679
+ container.insertAdjacentHTML('beforeend', toolHtml);
680
+ }
681
+ return;
682
+ }
683
+
684
+ // Get or create children container
685
+ let childrenContainer = taskGroup.querySelector('.task-group-children');
686
+ if (!childrenContainer) {
687
+ const body = taskGroup.querySelector('.task-group-body');
688
+ if (!body) {
689
+ console.error(`[ThinkingController] Task group body not found for ${taskId}`);
690
+ return;
691
+ }
692
+ childrenContainer = document.createElement('div');
693
+ childrenContainer.className = 'task-group-children';
694
+ body.appendChild(childrenContainer);
695
+ }
696
+
697
+ // Render and insert tool
698
+ const toolHtml = this._renderSingleTool(tool);
699
+ if (toolHtml) {
700
+ childrenContainer.insertAdjacentHTML('beforeend', toolHtml);
701
+ }
702
+
703
+ // Update count badge
704
+ this._updateTaskChildCount(taskGroup);
705
+ }
706
+
707
+ /**
708
+ * Update the child count badge on a Task group
709
+ */
710
+ _updateTaskChildCount(taskGroup) {
711
+ const children = taskGroup.querySelectorAll('.task-group-children .thinking-tool-card');
712
+ let countBadge = taskGroup.querySelector('.task-group-count');
713
+ const header = taskGroup.querySelector('.task-group-header');
714
+
715
+ const count = children.length;
716
+
717
+ if (count > 0) {
718
+ if (!countBadge) {
719
+ // Create count badge if it doesn't exist
720
+ const statusBadge = header.querySelector('.task-group-badge');
721
+ if (statusBadge) {
722
+ countBadge = document.createElement('span');
723
+ countBadge.className = 'task-group-count';
724
+ statusBadge.before(countBadge);
725
+ }
726
+ }
727
+ if (countBadge) {
728
+ countBadge.textContent = `${count} tool${count !== 1 ? 's' : ''}`;
729
+ countBadge.style.display = '';
730
+ }
731
+ } else if (countBadge) {
732
+ countBadge.style.display = 'none';
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Update a tool result (live update)
738
+ * @param {Object} msg - The thinking message
739
+ * @param {string} toolId - The tool ID
740
+ * @param {string} content - The tool result content
741
+ * @param {string[]|null} childToolIds - For Task results, list of child tool IDs
742
+ */
743
+ updateThinkingMessageToolResult(msg, toolId, content, childToolIds = null) {
744
+ // First try standard tool card selector
745
+ let toolCard = document.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
746
+
747
+ if (!toolCard) {
748
+ // Check if it's a Task group
749
+ const taskGroup = document.querySelector(`.task-group[data-task-id="${toolId}"]`);
750
+ if (taskGroup) {
751
+ this._updateTaskGroupResult(taskGroup, msg, toolId, content, childToolIds);
752
+ return;
753
+ }
754
+ // Tool not found - this shouldn't happen
755
+ console.warn(`[ThinkingController] Tool card not found for ${toolId}`);
756
+ return;
757
+ }
758
+
759
+ // Standard tool update
760
+ const tool = msg.tools?.find(t => t.toolId === toolId);
761
+ if (!tool) return;
762
+
763
+ const newHtml = this._renderSingleTool({
764
+ ...tool,
765
+ toolOutput: content,
766
+ toolCompleted: true
767
+ });
768
+
769
+ if (newHtml) {
770
+ toolCard.outerHTML = newHtml;
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Update a Task group when its result arrives
776
+ */
777
+ _updateTaskGroupResult(taskGroup, msg, toolId, content, childToolIds) {
778
+ // Mark as complete
779
+ taskGroup.setAttribute('data-complete', 'true');
780
+
781
+ // Update badge
782
+ const badge = taskGroup.querySelector('.task-group-badge');
783
+ if (badge) {
784
+ badge.className = 'task-group-badge task-group-badge-success';
785
+ badge.textContent = 'Done';
786
+ }
787
+
788
+ // Parse agent response and usage data
789
+ const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(content);
790
+
791
+ // Inject usage badges into header
792
+ if (usage) {
793
+ const header = taskGroup.querySelector('.task-group-header');
794
+ if (header) {
795
+ header.querySelectorAll('.task-group-tokens, .task-group-duration').forEach(el => el.remove());
796
+ const statusBadge = taskGroup.querySelector('.task-group-badge');
797
+ const tokText = formatTokensBadge(usage.totalTokens);
798
+ const durText = formatDuration(usage.durationMs);
799
+ const insertBefore = (el) => { if (statusBadge) statusBadge.before(el); else header.appendChild(el); };
800
+ if (durText) {
801
+ const durEl = document.createElement('span');
802
+ durEl.className = 'task-group-duration';
803
+ durEl.textContent = durText;
804
+ insertBefore(durEl);
805
+ }
806
+ if (tokText) {
807
+ const tokEl = document.createElement('span');
808
+ tokEl.className = 'task-group-tokens';
809
+ tokEl.textContent = tokText;
810
+ insertBefore(tokEl);
811
+ }
812
+ }
813
+ }
814
+
815
+ // Update or create preview
816
+ let preview = taskGroup.querySelector('.task-group-preview');
817
+ if (agentResponse) {
818
+ const strippedResponse = this._stripMarkdownSyntax(agentResponse);
819
+ const previewText = strippedResponse.slice(0, 150).trim();
820
+
821
+ if (!preview) {
822
+ const header = taskGroup.querySelector('.task-group-header');
823
+ preview = document.createElement('div');
824
+ preview.className = 'task-group-preview';
825
+ header.after(preview);
826
+ }
827
+ preview.textContent = previewText + (strippedResponse.length > 150 ? '...' : '');
828
+ }
829
+
830
+ // Update or create full response
831
+ let responseContainer = taskGroup.querySelector('.task-group-response');
832
+ const body = taskGroup.querySelector('.task-group-body');
833
+
834
+ if (agentResponse && body) {
835
+ if (!responseContainer) {
836
+ responseContainer = document.createElement('div');
837
+ responseContainer.className = 'task-group-response';
838
+ body.insertBefore(responseContainer, body.firstChild);
839
+ }
840
+ responseContainer.innerHTML = this._renderMarkdown(agentResponse);
841
+ }
842
+
843
+ // Verify/correct child tool grouping if server provided childToolIds
844
+ if (childToolIds && childToolIds.length > 0) {
845
+ this._verifyTaskChildren(taskGroup, childToolIds);
846
+ }
847
+
848
+ // Update count badge
849
+ this._updateTaskChildCount(taskGroup);
850
+ }
851
+
852
+ /**
853
+ * Verify and correct child tool grouping based on server-provided IDs
854
+ * This handles cases where tools were rendered flat during parallel execution
855
+ */
856
+ _verifyTaskChildren(taskGroup, childToolIds) {
857
+ const container = taskGroup.closest('.thinking-step-tools');
858
+ if (!container) return;
859
+
860
+ let childrenContainer = taskGroup.querySelector('.task-group-children');
861
+ if (!childrenContainer) {
862
+ const body = taskGroup.querySelector('.task-group-body');
863
+ if (!body) return;
864
+ childrenContainer = document.createElement('div');
865
+ childrenContainer.className = 'task-group-children';
866
+ body.appendChild(childrenContainer);
867
+ }
868
+
869
+ // Move any misplaced tools into this Task group
870
+ for (const toolId of childToolIds) {
871
+ // Skip if already in this Task group
872
+ if (childrenContainer.querySelector(`[data-tool-id="${toolId}"]`)) {
873
+ continue;
874
+ }
875
+
876
+ // Find the tool anywhere in the container (might be a sibling or in another Task)
877
+ const tool = container.querySelector(`.thinking-tool-card[data-tool-id="${toolId}"]`);
878
+ if (tool) {
879
+ // Move it into this Task's children
880
+ childrenContainer.appendChild(tool);
881
+ }
882
+ }
883
+ }
884
+
885
+ // ═══════════════════════════════════════════════════════════════
886
+ // LEGACY METHODS (compatibility)
887
+ // ═══════════════════════════════════════════════════════════════
888
+
889
+ renderThinkingBlock(msg) {
890
+ return this._renderSection([msg]);
891
+ }
892
+
893
+ renderThinkingGroup(thinkingMsgs) {
894
+ const div = document.createElement('div');
895
+ div.className = 'message thinking';
896
+ div.id = `thinking-group-${thinkingMsgs[0].id}`;
897
+ div.innerHTML = this._renderSection(thinkingMsgs);
898
+ this.ctx.getMessageContainer().appendChild(div);
899
+ }
900
+
901
+ renderThinkingEdits(thinkingMsgs) {
902
+ return ''; // Tools are inline with each step now
903
+ }
904
+
905
+ updateThinkingEditsSection(msg) {
906
+ // No-op - tools update inline
907
+ }
908
+
909
+ createThinkingGroupElement(thinkingGroup) {
910
+ const groupDiv = document.createElement('div');
911
+ groupDiv.className = 'message thinking';
912
+ groupDiv.innerHTML = this._renderSection(thinkingGroup);
913
+ return groupDiv;
914
+ }
915
+ }