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,1500 @@
1
+ /**
2
+ * Tool Renderer Module
3
+ * Renders tool blocks (Bash, Read, Edit, Write, etc.) with specialized displays
4
+ */
5
+
6
+ import S from './strings.js';
7
+ import { $, escapeHtml, escapeAttr, sanitizeSvg, parseTaskUsage, formatTokensBadge, formatDuration } from './utils.js';
8
+ import { CONFIG } from './config.js';
9
+ import { generateSmartDiff, renderSmartDiff } from './diff-utils.js';
10
+ // URL patterns always needed, path pattern for client-side scanning in colorized output
11
+ import { buildUrlPattern, buildPathPattern, buildStandalonePattern, isValidStandaloneFile, cleanUrlTrailingPunct, parseLineInfo } from './linkify-utils.js';
12
+ import { parseBackgroundTaskOutput, bgTaskTracker } from './background-tasks.js';
13
+ import { getToolCollapseMode, getToolCategory } from './widgets/config-widget.js';
14
+ import { blockMethods } from './tool-renderer-blocks.js';
15
+ import { thinkingMethods } from './tool-renderer-thinking.js';
16
+ import { basename } from './path-utils.js';
17
+
18
+ /**
19
+ * Clean tool_use_error XML wrappers from error/output text.
20
+ * Claude CLI wraps sibling errors as <tool_use_error>message</tool_use_error>.
21
+ * Returns {text, isSiblingError} — isSiblingError is true when another parallel tool failed.
22
+ */
23
+ export function cleanToolError(text) {
24
+ if (!text) return { text: '', isSiblingError: false };
25
+ if (typeof text !== 'string') text = (typeof text === 'object' && text !== null) ? JSON.stringify(text, null, 2) : String(text);
26
+ const match = text.match(/<tool_use_error>([\s\S]*?)<\/tool_use_error>/);
27
+ if (match) {
28
+ const inner = match[1].trim();
29
+ const isSiblingError = inner.toLowerCase().includes('sibling tool call errored');
30
+ return { text: inner, isSiblingError };
31
+ }
32
+ return { text, isSiblingError: false };
33
+ }
34
+
35
+ // ANSI color code to CSS class mapping
36
+ const ANSI_COLORS = {
37
+ '30': 'ansi-black', '31': 'ansi-red', '32': 'ansi-green', '33': 'ansi-yellow',
38
+ '34': 'ansi-blue', '35': 'ansi-magenta', '36': 'ansi-cyan', '37': 'ansi-white',
39
+ '90': 'ansi-bright-black', '91': 'ansi-bright-red', '92': 'ansi-bright-green',
40
+ '93': 'ansi-bright-yellow', '94': 'ansi-bright-blue', '95': 'ansi-bright-magenta',
41
+ '96': 'ansi-bright-cyan', '97': 'ansi-bright-white'
42
+ };
43
+
44
+ // File extension → highlight.js language mapping (shared across all tool renderers)
45
+ const LANG_MAP = {
46
+ js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
47
+ py: 'python', rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
48
+ c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
49
+ php: 'php', swift: 'swift', kt: 'kotlin', scala: 'scala',
50
+ html: 'xml', htm: 'xml', xml: 'xml', svg: 'xml',
51
+ css: 'css', scss: 'scss', less: 'less', sass: 'sass',
52
+ json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini', ini: 'ini',
53
+ md: 'markdown', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
54
+ dockerfile: 'dockerfile', makefile: 'makefile',
55
+ excalidraw: 'json',
56
+ // Image formats — not highlight.js languages, but used for display detection
57
+ png: 'image', jpg: 'image', jpeg: 'image', gif: 'image',
58
+ webp: 'image', ico: 'image', bmp: 'image', tiff: 'image',
59
+ heic: 'image', avif: 'image'
60
+ };
61
+
62
+ // Display overrides: when the hljs lang name isn't what users expect to see
63
+ const LANG_DISPLAY_OVERRIDE = { excalidraw: 'excalidraw' };
64
+
65
+ // Compound extension overrides (checked by filename, not just ext)
66
+ const COMPOUND_EXT_OVERRIDE = {
67
+ '.vl.json': { hlLang: 'json', displayLang: 'chart' },
68
+ };
69
+
70
+ export function getLangForExt(ext, filename) {
71
+ // Check compound extensions first (e.g., .vl.json → chart)
72
+ if (filename) {
73
+ const lower = filename.toLowerCase();
74
+ for (const [compound, result] of Object.entries(COMPOUND_EXT_OVERRIDE)) {
75
+ if (lower.endsWith(compound)) return result;
76
+ }
77
+ }
78
+ let hlLang = LANG_MAP[ext] || 'plaintext';
79
+ // Fall back to plaintext for languages not in the hljs bundle (e.g. scala, sass)
80
+ if (hlLang !== 'plaintext' && hlLang !== 'image' && window.hljs && !window.hljs.getLanguage(hlLang)) {
81
+ hlLang = 'plaintext';
82
+ }
83
+ const displayLang = LANG_DISPLAY_OVERRIDE[ext] || (LANG_MAP[ext] || 'plaintext');
84
+ return { hlLang, displayLang };
85
+ }
86
+
87
+ /**
88
+ * Convert ANSI escape codes to HTML spans
89
+ */
90
+ export function ansiToHtml(text) {
91
+ if (!text || !text.includes('\x1b[')) return null; // No ANSI codes
92
+
93
+ let result = '';
94
+ let currentClasses = [];
95
+ let i = 0;
96
+
97
+ while (i < text.length) {
98
+ // Check for ANSI escape sequence
99
+ if (text[i] === '\x1b' && text[i + 1] === '[') {
100
+ const endIdx = text.indexOf('m', i);
101
+ if (endIdx !== -1) {
102
+ const codes = text.slice(i + 2, endIdx).split(';');
103
+
104
+ // Close current span if any
105
+ if (currentClasses.length > 0) {
106
+ result += '</span>';
107
+ currentClasses = [];
108
+ }
109
+
110
+ // Process codes
111
+ for (const code of codes) {
112
+ if (code === '0' || code === '') {
113
+ // Reset
114
+ currentClasses = [];
115
+ } else if (code === '1') {
116
+ currentClasses.push('ansi-bold');
117
+ } else if (code === '3') {
118
+ currentClasses.push('ansi-italic');
119
+ } else if (code === '4') {
120
+ currentClasses.push('ansi-underline');
121
+ } else if (ANSI_COLORS[code]) {
122
+ currentClasses.push(ANSI_COLORS[code]);
123
+ }
124
+ }
125
+
126
+ // Open new span if we have classes
127
+ if (currentClasses.length > 0) {
128
+ result += `<span class="${currentClasses.join(' ')}">`;
129
+ }
130
+
131
+ i = endIdx + 1;
132
+ continue;
133
+ }
134
+ }
135
+
136
+ // Escape HTML special chars
137
+ const char = text[i];
138
+ if (char === '<') result += '&lt;';
139
+ else if (char === '>') result += '&gt;';
140
+ else if (char === '&') result += '&amp;';
141
+ else result += char;
142
+ i++;
143
+ }
144
+
145
+ // Close any remaining span
146
+ if (currentClasses.length > 0) {
147
+ result += '</span>';
148
+ }
149
+
150
+ return result;
151
+ }
152
+
153
+ /**
154
+ * Apply pattern-based colorization to bash output
155
+ */
156
+ export function colorizeBashLine(line, escapedLine) {
157
+ // Git diff patterns
158
+ if (/^\+(?!\+\+)/.test(line)) {
159
+ return `<span class="bash-diff-add">${escapedLine}</span>`;
160
+ }
161
+ if (/^-(?!--)/.test(line)) {
162
+ return `<span class="bash-diff-del">${escapedLine}</span>`;
163
+ }
164
+ if (/^@@\s/.test(line)) {
165
+ return `<span class="bash-diff-hunk">${escapedLine}</span>`;
166
+ }
167
+
168
+ // Git status patterns
169
+ if (/^\s*M\s+/.test(line) || /^\s*modified:\s+/i.test(line)) {
170
+ return `<span class="bash-git-modified">${escapedLine}</span>`;
171
+ }
172
+ if (/^\s*A\s+/.test(line) || /^\s*new file:\s+/i.test(line)) {
173
+ return `<span class="bash-git-added">${escapedLine}</span>`;
174
+ }
175
+ if (/^\s*D\s+/.test(line) || /^\s*deleted:\s+/i.test(line)) {
176
+ return `<span class="bash-git-deleted">${escapedLine}</span>`;
177
+ }
178
+ if (/^\?\?\s+/.test(line)) {
179
+ return `<span class="bash-git-untracked">${escapedLine}</span>`;
180
+ }
181
+
182
+ // Error patterns
183
+ if (/\b(error|Error|ERROR|fatal|FATAL|fail|FAIL|failed|FAILED)\b/.test(line)) {
184
+ return `<span class="bash-error-line">${escapedLine}</span>`;
185
+ }
186
+
187
+ // Warning patterns
188
+ if (/\b(warning|Warning|WARNING|warn|WARN)\b/.test(line)) {
189
+ return `<span class="bash-warn-line">${escapedLine}</span>`;
190
+ }
191
+
192
+ // Success patterns
193
+ if (/\b(success|Success|SUCCESS|passed|PASSED|ok|OK|done|Done|DONE)\b/.test(line)) {
194
+ return `<span class="bash-success-line">${escapedLine}</span>`;
195
+ }
196
+
197
+ return escapedLine;
198
+ }
199
+
200
+ /**
201
+ * ToolRenderer - Renders tool use blocks with specialized formatting
202
+ */
203
+ export class ToolRenderer {
204
+ /**
205
+ * @param {Object} options - Configuration
206
+ * @param {number} options.maxOutputLength - Max output chars before truncation
207
+ */
208
+ constructor(options = {}) {
209
+ this.maxOutputLength = options.maxOutputLength || CONFIG.MAX_OUTPUT_LENGTH || 3000;
210
+ }
211
+
212
+ /**
213
+ * Generate gutter icon HTML for tool collapse
214
+ * @private
215
+ */
216
+ _getGutterIconHtml(toolId) {
217
+ return `
218
+ <button class="tool-gutter-icon" data-act="toggle-normal-tool-collapse" data-id="${escapeAttr(toolId)}" data-tooltip="Collapse/expand this tool">
219
+ <svg class="gutter-collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
220
+ <path d="M19 9l-7 7-7-7"/>
221
+ </svg>
222
+ <svg class="gutter-expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
223
+ <path d="M9 5l7 7-7 7"/>
224
+ </svg>
225
+ </button>
226
+ `;
227
+ }
228
+
229
+ /**
230
+ * Wrap tool HTML with a container that has gutter icon
231
+ * @private
232
+ */
233
+ _wrapWithGutter(toolHtml, toolId, { collapseMode = 'compact', noGutterIcon = false } = {}) {
234
+ const gutterIcon = noGutterIcon ? '' : this._getGutterIconHtml(toolId);
235
+ const collapsedClass = collapseMode === 'collapsed' ? ' tool-collapsed' : '';
236
+ return `<div class="tool-block-wrapper${collapsedClass}" data-tool-id="${toolId}">${gutterIcon}${toolHtml}</div>`;
237
+ }
238
+
239
+ /**
240
+ * Render a tool block for a message
241
+ */
242
+ renderToolBlock(msg, { isThinking = false, isAgent = false } = {}) {
243
+ const toolId = `tool-${msg.id}`;
244
+ const toolType = getToolCategory(msg.toolName);
245
+ const context = isAgent ? 'agent' : isThinking ? 'thinking' : 'normal';
246
+ const mode = getToolCollapseMode(context, toolType);
247
+ const defaultExpanded = mode === 'expanded';
248
+ const gutterOpts = { collapseMode: mode };
249
+ let html;
250
+
251
+ // Special rendering for Edit tool - show colorized diff
252
+ if (msg.toolName === 'Edit' && msg.toolInput) {
253
+ // Check for sibling tool error
254
+ const editCleaned = cleanToolError(msg.toolOutput || msg.toolError || '');
255
+ if (editCleaned.isSiblingError) {
256
+ const fileName = (msg.toolInput.file_path || '').split('/').pop();
257
+ html = this._renderSiblingErrorBlock(msg, 'edit', escapeHtml(fileName));
258
+ return this._wrapWithGutter(html, toolId, gutterOpts);
259
+ }
260
+ const diffHtml = this.renderEditDiff(msg.toolInput, msg.toolOutput, msg.toolId, msg.startLine);
261
+ if (diffHtml) {
262
+ const hasOutput = msg.toolOutput || msg.toolError;
263
+ const isCompleted = msg.toolCompleted || hasOutput;
264
+ const cleanedEditErr = cleanToolError(msg.toolError);
265
+ const statusClass = msg.toolError ? 'edit-status-error' : (isCompleted ? 'edit-status-success' : 'edit-status-pending');
266
+ const statusText = msg.toolError ? 'Failed' : (isCompleted ? 'Applied' : 'Applying...');
267
+
268
+ html = `<div class="tool-block edit-tool-block" id="${toolId}">
269
+ ${diffHtml}
270
+ <div class="edit-status ${statusClass}">${statusText}${msg.toolError ? ': ' + escapeHtml(cleanedEditErr.text) : ''}</div>
271
+ </div>`;
272
+ return this._wrapWithGutter(html, toolId, gutterOpts);
273
+ }
274
+ }
275
+
276
+ // Special rendering for TodoWrite tool - show nice todo list
277
+ if (msg.toolName === 'TodoWrite' && msg.toolInput?.todos) {
278
+ html = this.renderTodoBlock(msg);
279
+ return this._wrapWithGutter(html, toolId, gutterOpts);
280
+ }
281
+
282
+ // Special rendering for Read tool with image files - add View button
283
+ if (msg.toolName === 'Read' && msg.toolInput?.file_path) {
284
+ const filePath = msg.toolInput.file_path;
285
+ const isImage = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i.test(filePath);
286
+ if (isImage) {
287
+ html = this.renderReadImageBlock(msg);
288
+ return this._wrapWithGutter(html, toolId, gutterOpts);
289
+ }
290
+ // Non-image Read tool - always use styled block (handles loading state too)
291
+ html = this.renderReadBlock(msg, { defaultExpanded });
292
+ return this._wrapWithGutter(html, toolId, gutterOpts);
293
+ }
294
+
295
+ // Specialized compact rendering for common tools
296
+ if ((msg.toolName === 'Bash' || msg.toolName === 'Shell') && msg.toolInput?.command) {
297
+ html = this.renderBashBlock(msg, { defaultExpanded });
298
+ return this._wrapWithGutter(html, toolId, gutterOpts);
299
+ }
300
+ if (msg.toolName === 'Grep' && msg.toolInput?.pattern) {
301
+ html = this.renderGrepBlock(msg, { defaultExpanded });
302
+ return this._wrapWithGutter(html, toolId, gutterOpts);
303
+ }
304
+ if (msg.toolName === 'Glob' && msg.toolInput?.pattern) {
305
+ html = this.renderGlobBlock(msg, { defaultExpanded });
306
+ return this._wrapWithGutter(html, toolId, gutterOpts);
307
+ }
308
+ if (msg.toolName === 'Write' && msg.toolInput?.file_path) {
309
+ html = this.renderWriteBlock(msg, { defaultExpanded });
310
+ return this._wrapWithGutter(html, toolId, gutterOpts);
311
+ }
312
+ if (msg.toolName === 'WebFetch' || msg.toolName === 'WebSearch') {
313
+ html = this.renderWebFetchBlock(msg, { defaultExpanded });
314
+ return this._wrapWithGutter(html, toolId, gutterOpts);
315
+ }
316
+
317
+ // Special rendering for Task (agent) tool - show grouped style
318
+ // No gutter icon on Task block itself; sub-agent tools get their own gutter icons
319
+ if (msg.toolName === 'Task') {
320
+ html = this.renderTaskBlock(msg);
321
+ return this._wrapWithGutter(html, toolId, { collapseMode: mode, noGutterIcon: true });
322
+ }
323
+
324
+ // Compact rendering for Skill tool
325
+ if (msg.toolName === 'Skill') {
326
+ html = this.renderSkillBlock(msg);
327
+ return this._wrapWithGutter(html, toolId, gutterOpts);
328
+ }
329
+
330
+ // Compact rendering for TaskOutput tool
331
+ if (msg.toolName === 'TaskOutput') {
332
+ html = this.renderTaskOutputBlock(msg);
333
+ return this._wrapWithGutter(html, toolId, gutterOpts);
334
+ }
335
+
336
+ // Compact rendering for TaskStop tool
337
+ if (msg.toolName === 'TaskStop') {
338
+ html = this.renderTaskStopBlock(msg);
339
+ return this._wrapWithGutter(html, toolId, gutterOpts);
340
+ }
341
+
342
+ // Compact rendering for EnterPlanMode
343
+ if (msg.toolName === 'EnterPlanMode') {
344
+ html = this.renderEnterPlanBlock(msg);
345
+ return this._wrapWithGutter(html, toolId, gutterOpts);
346
+ }
347
+
348
+ // Compact rendering for AskUserQuestion (safety net — normally rendered as interactive form)
349
+ if (msg.toolName === 'AskUserQuestion') {
350
+ const questions = msg.toolInput?.questions || [];
351
+ const label = questions.length === 1
352
+ ? escapeHtml(questions[0].question || 'Question')
353
+ : `${questions.length} questions`;
354
+ html = `<div class="tool-block compact-tool-block" id="${toolId}">
355
+ <div class="tool-header compact-header"><span class="tool-name compact-tool-name">
356
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
357
+ AskUserQuestion<span class="tool-header-path">${label}</span></span></div></div>`;
358
+ return this._wrapWithGutter(html, toolId, gutterOpts);
359
+ }
360
+
361
+ // Compact rendering for ExitPlanMode (safety net — normally rendered as plan approval card)
362
+ if (msg.toolName === 'ExitPlanMode') {
363
+ html = `<div class="tool-block compact-tool-block" id="${toolId}">
364
+ <div class="tool-header compact-header"><span class="tool-name compact-tool-name">
365
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
366
+ ExitPlanMode<span class="tool-header-path">Plan ready for approval</span></span></div></div>`;
367
+ return this._wrapWithGutter(html, toolId, gutterOpts);
368
+ }
369
+
370
+ // Default tool block rendering
371
+ const inputDisplay = this.formatToolInput(msg.toolName, msg.toolInput);
372
+ const hasOutput = msg.toolOutput || msg.toolError;
373
+ const isCompleted = msg.toolCompleted || hasOutput;
374
+ const defaultCleaned = cleanToolError(msg.toolOutput || msg.toolError || '');
375
+ const outputContent = hasOutput
376
+ ? (defaultCleaned.isSiblingError
377
+ ? `<span class="bash-sibling-label">${S.tool_renderer.status.skipped}</span> a parallel tool call failed`
378
+ : this.linkifyToolOutput(escapeHtml(this.truncateOutput(defaultCleaned.text)), msg.fileLinks))
379
+ : (isCompleted ? S.tool_renderer.badges.done : '');
380
+ const outputClass = `tool-output${msg.toolError ? ' error' : ''}${(msg.toolOutput?.length || 0) > this.maxOutputLength ? ' truncated' : ''}`;
381
+
382
+ // Get compact header preview for all tools
383
+ const headerPreview = this.getToolHeaderPreview(msg.toolName, msg.toolInput);
384
+
385
+ // Add buttons for file-related tools (Read, Write, Edit)
386
+ const hasFilePath = msg.toolInput?.file_path;
387
+ const isFileViewable = (msg.toolName === 'Read' || msg.toolName === 'Write' || msg.toolName === 'Edit') && hasFilePath;
388
+ // Model-controlled path: stash quote-safe in data-file and read it back
389
+ // via this.dataset.file at click time (never interpolated into the JS
390
+ // string), so a path with `"`/`'` can't break out. Same pattern as fileLink().
391
+ const filePathAttr = hasFilePath ? escapeAttr(msg.toolInput.file_path) : '';
392
+
393
+ // "Open in Editor" button - opens file in editor tab
394
+ const editorButton = isFileViewable
395
+ ? `<button class="tool-action-btn" data-act="open-in-editor" data-file="${filePathAttr}" data-tooltip="Open in editor">
396
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 9.5-9.5z"/></svg>
397
+ </button>`
398
+ : '';
399
+
400
+ // "View" button - opens file preview widget
401
+ const viewButton = isFileViewable
402
+ ? `<button class="tool-action-btn" data-act="preview-file" data-file="${filePathAttr}" data-tooltip="Preview file">
403
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
404
+ </button>`
405
+ : '';
406
+
407
+ html = `<div class="tool-block" id="${toolId}">
408
+ <div class="tool-header" data-act="toggle-tool" data-id="${escapeAttr(msg.id)}">
409
+ <span class="tool-name">
410
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
411
+ ${escapeHtml(msg.toolName)}${headerPreview ? `<span class="tool-header-path">${headerPreview}</span>` : ''}
412
+ </span>
413
+ <div class="tool-actions">
414
+ ${editorButton}${viewButton}<button class="tool-action-btn" data-act="copy-tool-output" data-id="${escapeAttr(msg.id)}" data-tooltip="Copy">
415
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
416
+ </button>
417
+ </div>
418
+ </div>
419
+ <div class="tool-content">
420
+ <div class="tool-input">
421
+ ${inputDisplay}
422
+ </div>
423
+ ${isCompleted ? `<div class="${outputClass}">${outputContent}</div>` : '<div class="tool-output" style="color: var(--text-muted)">Running...</div>'}
424
+ </div>
425
+ </div>`;
426
+ return this._wrapWithGutter(html, toolId, gutterOpts);
427
+ }
428
+
429
+ /**
430
+ * Get a compact preview string for the tool header
431
+ */
432
+ getToolHeaderPreview(toolName, input) {
433
+ if (!input) return '';
434
+
435
+ let preview = '';
436
+ switch (toolName) {
437
+ case 'Bash':
438
+ case 'Shell':
439
+ preview = input.command || '';
440
+ break;
441
+ case 'Read':
442
+ // Show just filename
443
+ preview = basename(input.file_path) || '';
444
+ break;
445
+ case 'Write':
446
+ preview = basename(input.file_path) || '';
447
+ break;
448
+ case 'Grep':
449
+ preview = `/${input.pattern || ''}/`;
450
+ break;
451
+ case 'Glob':
452
+ preview = input.pattern || '';
453
+ break;
454
+ case 'WebFetch':
455
+ // Show domain only
456
+ try {
457
+ preview = new URL(input.url).hostname;
458
+ } catch { preview = input.url || ''; }
459
+ break;
460
+ case 'Task':
461
+ preview = input.description || '';
462
+ break;
463
+ case 'Skill':
464
+ preview = input.skill || '';
465
+ break;
466
+ default:
467
+ // Try common field names
468
+ preview = input.description || input.command || input.path || basename(input.file_path) || '';
469
+ }
470
+
471
+ // Truncate and escape
472
+ if (preview.length > 50) {
473
+ preview = preview.substring(0, 47) + '...';
474
+ }
475
+ return escapeHtml(preview);
476
+ }
477
+
478
+ /**
479
+ * Generate a clickable filename link for tool headers.
480
+ * Returns an <a> with file-path-link class so the context menu system picks it up.
481
+ * @param {string} fullPath - Full file path
482
+ * @param {string} displayName - Display text (usually just filename)
483
+ * @param {string} extraClass - Additional CSS class (e.g., 'read-filename')
484
+ */
485
+ fileLink(fullPath, displayName, extraClass = '') {
486
+ // SECURITY: fullPath is model-controlled (e.g. a Write/Edit target shown
487
+ // on the permission-approval card). Keep it out of inline JS entirely —
488
+ // stash it in a quote-escaped data-file attribute and read it back via
489
+ // this.dataset.file at click time, so a path containing `"`/`'` can't
490
+ // break out of the attribute or the onclick handler. escapeAttr covers
491
+ // quotes; escapeHtml only covers & < >.
492
+ const attrPath = escapeAttr(fullPath);
493
+ const classes = `file-path-link${extraClass ? ' ' + extraClass : ''}`;
494
+ return `<a href="#" class="${classes}" data-act="preview-file" data-file="${attrPath}" data-tooltip="${attrPath}">${escapeHtml(displayName)}</a>`;
495
+ }
496
+
497
+ /**
498
+ * Format tool input for display
499
+ */
500
+ formatToolInput(toolName, input) {
501
+ if (!input) return '';
502
+
503
+ if ((toolName === 'Bash' || toolName === 'Shell') && input.command) {
504
+ return `<code>$ ${escapeHtml(input.command)}</code>`;
505
+ }
506
+ if (toolName === 'Read' && input.file_path) {
507
+ return `<code>${escapeHtml(input.file_path)}</code>`;
508
+ }
509
+ if (toolName === 'Grep' && input.pattern) {
510
+ return `<code>/${escapeHtml(input.pattern)}/</code>`;
511
+ }
512
+ if (toolName === 'Glob' && input.pattern) {
513
+ return `<code>${escapeHtml(input.pattern)}</code>`;
514
+ }
515
+ if (toolName === 'Write' && input.file_path) {
516
+ return `<code>${escapeHtml(input.file_path)}</code>`;
517
+ }
518
+ if (toolName === 'Edit' && input.file_path) {
519
+ return `<code>${escapeHtml(input.file_path)}</code>`;
520
+ }
521
+
522
+ return this._renderInputTable(input);
523
+ }
524
+
525
+ /**
526
+ * Render a tool input object as a key→value table.
527
+ * Top-level keys are rows; nested objects/arrays are JSON-stringified inline.
528
+ */
529
+ _renderInputTable(input) {
530
+ if (typeof input !== 'object' || Array.isArray(input)) {
531
+ return `<pre>${escapeHtml(JSON.stringify(input, null, 2))}</pre>`;
532
+ }
533
+ const keys = Object.keys(input);
534
+ if (keys.length === 0) return '';
535
+
536
+ const rows = keys.map(key => {
537
+ const value = input[key];
538
+ let cellHtml;
539
+ if (value === null) {
540
+ cellHtml = '<span class="tool-input-null">null</span>';
541
+ } else if (typeof value === 'string') {
542
+ cellHtml = escapeHtml(value);
543
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
544
+ cellHtml = `<span class="tool-input-scalar">${escapeHtml(String(value))}</span>`;
545
+ } else {
546
+ cellHtml = `<code class="tool-input-nested">${escapeHtml(JSON.stringify(value))}</code>`;
547
+ }
548
+ return `<tr><th class="tool-input-key">${escapeHtml(key)}</th><td class="tool-input-value">${cellHtml}</td></tr>`;
549
+ }).join('');
550
+
551
+ return `<table class="tool-input-table">${rows}</table>`;
552
+ }
553
+
554
+ /**
555
+ * Format tool input for thinking message (compact display)
556
+ */
557
+ formatThinkingToolInput(toolName, input) {
558
+ if (!input) return '';
559
+
560
+ if ((toolName === 'Bash' || toolName === 'Shell') && input.command) {
561
+ return `<code>$ ${escapeHtml(input.command.slice(0, 60))}${input.command.length > 60 ? '...' : ''}</code>`;
562
+ }
563
+ if (toolName === 'Read' && input.file_path) {
564
+ return `<code>${escapeHtml(basename(input.file_path))}</code>`;
565
+ }
566
+ if (toolName === 'Write' && input.file_path) {
567
+ return `<code>${escapeHtml(basename(input.file_path))}</code>`;
568
+ }
569
+ if (toolName === 'Edit' && input.file_path) {
570
+ return `<code>${escapeHtml(basename(input.file_path))}</code>`;
571
+ }
572
+ if (toolName === 'Grep' && input.pattern) {
573
+ return `<code>/${escapeHtml(input.pattern.slice(0, 30))}${input.pattern.length > 30 ? '...' : ''}/</code>`;
574
+ }
575
+ if (toolName === 'Glob' && input.pattern) {
576
+ return `<code>${escapeHtml(input.pattern)}</code>`;
577
+ }
578
+ if (toolName === 'Task') {
579
+ return `<span class="thinking-tool-desc">${escapeHtml(input.description || '')}</span>`;
580
+ }
581
+
582
+ // For other tools, show first key-value pair
583
+ const keys = Object.keys(input);
584
+ if (keys.length > 0) {
585
+ const key = keys[0];
586
+ const val = String(input[key]).slice(0, 40);
587
+ return `<span class="thinking-tool-param">${key}: ${escapeHtml(val)}${String(input[key]).length > 40 ? '...' : ''}</span>`;
588
+ }
589
+
590
+ return '';
591
+ }
592
+
593
+ /**
594
+ * Linkify file paths and URLs in tool output (already HTML-escaped text).
595
+ * Supports two modes:
596
+ * 1. Position-based: Uses server-provided positions (fileLinks array)
597
+ * 2. Client-scan: Scans text for paths and verifies against a map (verifiedFiles object)
598
+ * @param {string} text - The HTML-escaped tool output text
599
+ * @param {Array|null} fileLinks - Server-provided file links [{path, resolved, start, end, line_info}]
600
+ * @param {Object|null} verifiedFiles - Map of {path: resolvedPath} for client-side scanning
601
+ */
602
+ linkifyToolOutput(text, fileLinks = null, verifiedFiles = null) {
603
+ const replacements = []; // [{start, end, html}]
604
+
605
+ // 1. Find URLs (scanned client-side, no server verification needed)
606
+ const urlPattern = buildUrlPattern();
607
+ let match;
608
+ while ((match = urlPattern.exec(text)) !== null) {
609
+ let url = match[0];
610
+ const cleaned = cleanUrlTrailingPunct(url);
611
+ url = cleaned.url;
612
+ const end = match.index + url.length;
613
+
614
+ replacements.push({
615
+ start: match.index,
616
+ end: end,
617
+ // href quote-safe; text escaped. (The URL regex already
618
+ // constrains the scheme to http/https.)
619
+ html: `<a href="${escapeAttr(url)}" class="external-link" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`
620
+ });
621
+ }
622
+
623
+ // 2a. Add file links using server-provided positions (when available)
624
+ if (fileLinks && Array.isArray(fileLinks)) {
625
+ for (const link of fileLinks) {
626
+ const { path, resolved, start, end, line_info } = link;
627
+
628
+ // Check for overlap with URL replacements
629
+ const overlaps = replacements.some(r =>
630
+ (start < r.end && end > r.start)
631
+ );
632
+ if (overlaps) continue;
633
+
634
+ const fullDisplay = line_info ? path + line_info : path;
635
+ // Preview path (resolved || path) is read at click time from
636
+ // this.dataset — never interpolated into the JS string.
637
+ const resolvedAttr = resolved ? ` data-resolved="${escapeAttr(resolved)}"` : '';
638
+
639
+ // Parse line info into options for preview
640
+ const lineOpts = parseLineInfo(line_info);
641
+ const optsJson = lineOpts ? JSON.stringify(lineOpts).replace(/"/g, '&quot;') : '';
642
+ const optsAttr = optsJson ? ` data-line-opts="${optsJson}"` : '';
643
+
644
+ replacements.push({
645
+ start,
646
+ end,
647
+ html: `<a href="#" class="file-path-link" data-act="open-file-link" data-file="${escapeAttr(path)}"${resolvedAttr} data-tooltip="${escapeAttr(resolved || path)}"${optsAttr}>${escapeHtml(fullDisplay)}</a>`
648
+ });
649
+ }
650
+ }
651
+
652
+ // 2b. Client-side path scanning (for colorized output where positions don't work)
653
+ if (verifiedFiles && Object.keys(verifiedFiles).length > 0) {
654
+ // Helper to add a verified path replacement
655
+ const addPathReplacement = (matchObj, path, lineInfo, fullMatch) => {
656
+ const resolvedPath = verifiedFiles[path];
657
+ if (!resolvedPath) return;
658
+
659
+ // Check for overlap with existing replacements
660
+ const overlaps = replacements.some(r =>
661
+ (matchObj.index < r.end && matchObj.index + fullMatch.length > r.start)
662
+ );
663
+ if (overlaps) return;
664
+
665
+ const fullDisplay = lineInfo ? path + lineInfo : path;
666
+
667
+ const lineOpts = parseLineInfo(lineInfo);
668
+ const optsJson = lineOpts ? JSON.stringify(lineOpts).replace(/"/g, '&quot;') : '';
669
+ const optsAttr = optsJson ? ` data-line-opts="${optsJson}"` : '';
670
+
671
+ replacements.push({
672
+ start: matchObj.index,
673
+ end: matchObj.index + fullMatch.length,
674
+ html: `<a href="#" class="file-path-link" data-act="open-file-link" data-file="${escapeAttr(path)}" data-resolved="${escapeAttr(resolvedPath)}" data-tooltip="${escapeAttr(resolvedPath)}"${optsAttr}>${escapeHtml(fullDisplay)}</a>`
675
+ });
676
+ };
677
+
678
+ // Scan for paths with directories (e.g., static/js/app.js)
679
+ const pathPattern = buildPathPattern();
680
+ while ((match = pathPattern.exec(text)) !== null) {
681
+ const path = match[1];
682
+ const lineInfo = match[2] || '';
683
+
684
+ if (!path.includes('/')) continue;
685
+ if (/^\d+\.\d+/.test(path)) continue;
686
+ if (/^\d+\/\d+/.test(path)) continue;
687
+
688
+ addPathReplacement(match, path, lineInfo, match[0]);
689
+ }
690
+
691
+ // Scan for standalone filenames (e.g., CLAUDE.md, server.py)
692
+ const standalonePattern = buildStandalonePattern();
693
+ while ((match = standalonePattern.exec(text)) !== null) {
694
+ const filename = match[1];
695
+ const lineInfo = match[2] || '';
696
+
697
+ if (!isValidStandaloneFile(filename, text.slice(Math.max(0, match.index - 20), match.index))) continue;
698
+
699
+ addPathReplacement(match, filename, lineInfo, match[0]);
700
+ }
701
+ }
702
+
703
+ // 3. Sort by position (descending) and apply replacements
704
+ replacements.sort((a, b) => b.start - a.start);
705
+ for (const r of replacements) {
706
+ text = text.slice(0, r.start) + r.html + text.slice(r.end);
707
+ }
708
+
709
+ return text;
710
+ }
711
+
712
+ /**
713
+ * Render a colorized diff for Edit tool operations.
714
+ * Shows removed lines in red, added lines in green, with stats and file link.
715
+ * @param {object} input - Tool input with file_path, old_string, new_string
716
+ * @param {string} toolOutput - Tool output for line number parsing
717
+ * @param {string} [toolId] - Tool ID for linking to Changes panel
718
+ */
719
+ parseEditLineNumber(toolOutput, newString) {
720
+ if (!toolOutput || !newString) return null;
721
+
722
+ // Extract "NUMBER<delim>content" pairs — Claude CLI uses either → (older) or \t (current)
723
+ const linePattern = /^\s*(\d+)(?:→|\t)(.*)$/gm;
724
+ const firstNewLine = newString.split('\n')[0];
725
+
726
+ let match;
727
+ while ((match = linePattern.exec(toolOutput)) !== null) {
728
+ const lineNum = parseInt(match[1], 10);
729
+ const content = match[2];
730
+ // Check if this line matches first line of new_string (trim to handle padding)
731
+ if (content.trim() === firstNewLine.trim()) {
732
+ return lineNum;
733
+ }
734
+ }
735
+
736
+ return null; // Unknown — caller hides line numbers rather than faking 1-based ones
737
+ }
738
+
739
+ /**
740
+ * Render a Read tool block for image files with inline preview
741
+ */
742
+ _renderSiblingErrorBlock(msg, toolType, label) {
743
+ const icons = {
744
+ grep: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
745
+ glob: '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
746
+ read: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>',
747
+ write: '<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/>',
748
+ edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
749
+ };
750
+ const icon = icons[toolType] || icons.read;
751
+ return `<div class="tool-block ${toolType}-block sibling-error" id="tool-${msg.id}">
752
+ <div class="${toolType}-header">
753
+ <svg class="${toolType}-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">${icon}</svg>
754
+ <code class="${toolType}-pattern">${label}</code>
755
+ <span class="sibling-error-badge">Skipped</span>
756
+ </div>
757
+ <div class="sibling-error-msg">A parallel tool call failed</div>
758
+ </div>`;
759
+ }
760
+
761
+ /**
762
+ * Render a compact Bash/Shell tool block
763
+ * Design: Clickable command copies, expandable for long/multiline commands
764
+ */
765
+ _parseTaskOutputXml(text) {
766
+ if (!text) return {};
767
+ const result = {};
768
+ const tagRegex = /<(\w+)>([\s\S]*?)<\/\1>/g;
769
+ let match;
770
+ while ((match = tagRegex.exec(text)) !== null) {
771
+ result[match[1]] = match[2].trim();
772
+ }
773
+ return result;
774
+ }
775
+
776
+ /**
777
+ * Parse agent streaming JSONL output into useful components.
778
+ * Extracts slug, text responses, tool counts, and token usage.
779
+ */
780
+ _parseAgentStream(rawOutput) {
781
+ const result = {
782
+ slug: '',
783
+ texts: [],
784
+ toolCounts: {},
785
+ totalInputTokens: 0,
786
+ totalOutputTokens: 0,
787
+ isTruncated: false,
788
+ truncatedPath: '',
789
+ };
790
+
791
+ if (!rawOutput) return result;
792
+
793
+ // Check for truncation marker at start
794
+ const truncMatch = rawOutput.match(/^\[Truncated\.\s*Full output:\s*(.+?)\]/);
795
+ if (truncMatch) {
796
+ result.isTruncated = true;
797
+ result.truncatedPath = truncMatch[1];
798
+ rawOutput = rawOutput.slice(truncMatch[0].length);
799
+ }
800
+
801
+ const lines = rawOutput.split('\n');
802
+ for (const line of lines) {
803
+ const trimmed = line.trim();
804
+ if (!trimmed || !trimmed.startsWith('{')) continue;
805
+
806
+ try {
807
+ const obj = JSON.parse(trimmed);
808
+
809
+ // Extract slug from first occurrence
810
+ if (obj.slug && !result.slug) {
811
+ result.slug = obj.slug;
812
+ }
813
+
814
+ // Parse assistant messages for text and tool_use
815
+ if (obj.message?.role === 'assistant' && obj.message?.content) {
816
+ for (const block of obj.message.content) {
817
+ if (block.type === 'text' && block.text) {
818
+ result.texts.push(block.text);
819
+ }
820
+ if (block.type === 'tool_use' && block.name) {
821
+ result.toolCounts[block.name] = (result.toolCounts[block.name] || 0) + 1;
822
+ }
823
+ }
824
+ }
825
+
826
+ // Accumulate token usage
827
+ if (obj.message?.usage) {
828
+ const u = obj.message.usage;
829
+ result.totalOutputTokens += u.output_tokens || 0;
830
+ result.totalInputTokens += (u.input_tokens || 0) +
831
+ (u.cache_read_input_tokens || 0) +
832
+ (u.cache_creation_input_tokens || 0);
833
+ }
834
+ } catch {
835
+ // Malformed JSON line (possibly truncated mid-stream), skip
836
+ }
837
+ }
838
+
839
+ return result;
840
+ }
841
+
842
+ _parseTaskOutputWithUsage(toolOutput) {
843
+ const raw = this._parseTaskOutput(toolOutput);
844
+ return parseTaskUsage(raw);
845
+ }
846
+
847
+ /**
848
+ * Parse Task toolOutput to extract text content
849
+ * Output format is usually: [{"type":"text","text":"..."}]
850
+ */
851
+ _parseTaskOutput(toolOutput) {
852
+ if (!toolOutput) return '';
853
+
854
+ try {
855
+ let parsed = toolOutput;
856
+ if (typeof toolOutput === 'string') {
857
+ if (toolOutput.startsWith('[')) {
858
+ parsed = JSON.parse(toolOutput);
859
+ } else {
860
+ return toolOutput;
861
+ }
862
+ }
863
+
864
+ if (Array.isArray(parsed)) {
865
+ return parsed
866
+ .filter(block => block.type === 'text' && block.text)
867
+ .map(block => block.text)
868
+ .join('\n\n');
869
+ }
870
+
871
+ if (parsed && typeof parsed === 'object' && parsed.text) {
872
+ return parsed.text;
873
+ }
874
+
875
+ return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
876
+ } catch (e) {
877
+ return typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput);
878
+ }
879
+ }
880
+
881
+ /**
882
+ * Strip markdown syntax for clean preview text
883
+ */
884
+ _stripMarkdownForPreview(text) {
885
+ if (!text) return '';
886
+ return text
887
+ .replace(/^#{1,6}\s+/gm, '')
888
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
889
+ .replace(/\*([^*]+)\*/g, '$1')
890
+ .replace(/__([^_]+)__/g, '$1')
891
+ .replace(/_([^_]+)_/g, '$1')
892
+ .replace(/`([^`]+)`/g, '$1')
893
+ .replace(/^>\s*/gm, '')
894
+ .replace(/^[-*+]\s+/gm, '')
895
+ .replace(/^\d+\.\s+/gm, '')
896
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
897
+ .replace(/\s+/g, ' ')
898
+ .trim();
899
+ }
900
+
901
+ /**
902
+ * Render markdown content safely
903
+ */
904
+ _renderMarkdownSafe(content) {
905
+ if (!content) return '';
906
+ // Try to use app's markdown renderer if available
907
+ if (window.app?.markdown) {
908
+ return window.app.markdown.render(content);
909
+ }
910
+ // Fallback: escape and preserve whitespace
911
+ return `<pre style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
912
+ }
913
+
914
+ /**
915
+ * Update a Task block result while preserving nested children
916
+ * Called when Task completes to show response without losing nested tools
917
+ */
918
+ _updateTaskBlockResult(taskBlock, msg) {
919
+ const isError = !!msg.toolError;
920
+ const isCompleted = msg.toolCompleted || msg.toolOutput || isError;
921
+
922
+ // Remove live progress indicator (replaced by final result)
923
+ const progressEl = taskBlock.querySelector('.task-progress');
924
+ if (progressEl) progressEl.remove();
925
+
926
+ // Update data-complete attribute
927
+ taskBlock.setAttribute('data-complete', isCompleted ? 'true' : 'false');
928
+
929
+ // Update status badge
930
+ const badge = taskBlock.querySelector('.task-block-badge');
931
+ if (badge) {
932
+ if (isError) {
933
+ badge.className = 'task-block-badge task-block-badge-error';
934
+ badge.textContent = S.tool_renderer.badges.failed;
935
+ } else if (isCompleted) {
936
+ badge.className = 'task-block-badge task-block-badge-success';
937
+ badge.textContent = S.tool_renderer.badges.done;
938
+ }
939
+ }
940
+
941
+ // Parse agent response and usage data
942
+ const { text: agentResponse, usage } = this._parseTaskOutputWithUsage(msg.toolOutput);
943
+
944
+ // Inject usage badges into header
945
+ if (usage) {
946
+ const header = taskBlock.querySelector('.task-block-header');
947
+ if (header) {
948
+ header.querySelectorAll('.task-block-tokens, .task-block-duration').forEach(el => el.remove());
949
+ const statusBadge = taskBlock.querySelector('.task-block-badge');
950
+ const tokText = formatTokensBadge(usage.totalTokens);
951
+ const durText = formatDuration(usage.durationMs);
952
+ const insertBefore = (el) => { if (statusBadge) statusBadge.before(el); else header.appendChild(el); };
953
+ if (durText) {
954
+ const durEl = document.createElement('span');
955
+ durEl.className = 'task-block-duration';
956
+ durEl.textContent = durText;
957
+ insertBefore(durEl);
958
+ }
959
+ if (tokText) {
960
+ const tokEl = document.createElement('span');
961
+ tokEl.className = 'task-block-tokens';
962
+ tokEl.textContent = tokText;
963
+ insertBefore(tokEl);
964
+ }
965
+ }
966
+ }
967
+
968
+ // Update or create preview
969
+ let preview = taskBlock.querySelector('.task-block-preview');
970
+ if (isCompleted && agentResponse && !isError) {
971
+ const strippedResponse = this._stripMarkdownForPreview(agentResponse);
972
+ const previewText = strippedResponse.slice(0, 150).trim();
973
+
974
+ if (!preview) {
975
+ const header = taskBlock.querySelector('.task-block-header');
976
+ preview = document.createElement('div');
977
+ preview.className = 'task-block-preview';
978
+ header.after(preview);
979
+ }
980
+ preview.textContent = previewText + (strippedResponse.length > 150 ? '...' : '');
981
+ }
982
+
983
+ // Update or create error display
984
+ let errorEl = taskBlock.querySelector('.task-block-error');
985
+ if (isError && msg.toolError) {
986
+ if (!errorEl) {
987
+ const header = taskBlock.querySelector('.task-block-header');
988
+ errorEl = document.createElement('div');
989
+ errorEl.className = 'task-block-error';
990
+ // Insert after header (or after preview if it exists)
991
+ const insertAfter = preview || header;
992
+ insertAfter.after(errorEl);
993
+ }
994
+ errorEl.textContent = msg.toolError;
995
+ } else if (errorEl) {
996
+ errorEl.remove();
997
+ }
998
+
999
+ // Update or create full response in body (preserving children)
1000
+ let body = taskBlock.querySelector('.task-block-body');
1001
+ if (!body) {
1002
+ body = document.createElement('div');
1003
+ body.className = 'task-block-body';
1004
+ taskBlock.appendChild(body);
1005
+ }
1006
+
1007
+ let responseContainer = body.querySelector('.task-block-response');
1008
+ if (isCompleted && agentResponse) {
1009
+ if (!responseContainer) {
1010
+ responseContainer = document.createElement('div');
1011
+ responseContainer.className = 'task-block-response';
1012
+ // Insert at beginning of body (before children)
1013
+ body.insertBefore(responseContainer, body.firstChild);
1014
+ }
1015
+ responseContainer.innerHTML = this._renderMarkdownSafe(agentResponse);
1016
+ }
1017
+
1018
+ // Add expand chevron if there's expandable content and it doesn't exist
1019
+ const hasExpandableContent = agentResponse || isError || body.querySelector('.task-block-children');
1020
+ const header = taskBlock.querySelector('.task-block-header');
1021
+ let expandChevron = header?.querySelector('.task-block-expand');
1022
+ if (hasExpandableContent && header && !expandChevron) {
1023
+ const chevron = document.createElement('span');
1024
+ chevron.className = 'task-block-expand';
1025
+ chevron.textContent = '›';
1026
+ header.appendChild(chevron);
1027
+ }
1028
+ }
1029
+
1030
+ /**
1031
+ * Update a Task block with live progress from task_progress events.
1032
+ * Shows current activity, tool count, and duration between header and body.
1033
+ */
1034
+ updateTaskProgress(taskBlock, progress) {
1035
+ // Don't update completed tasks
1036
+ if (taskBlock.getAttribute('data-complete') === 'true') return;
1037
+
1038
+ let el = taskBlock.querySelector('.task-progress');
1039
+ if (!el) {
1040
+ el = document.createElement('div');
1041
+ el.className = 'task-progress';
1042
+ const header = taskBlock.querySelector('.task-block-header');
1043
+ if (header) header.after(el);
1044
+ else return;
1045
+ }
1046
+
1047
+ // Tool icon based on last tool name
1048
+ const toolIcons = {
1049
+ Read: 'file', Edit: 'pencil', Write: 'pencil', Bash: 'terminal',
1050
+ Grep: 'search', Glob: 'search', WebFetch: 'globe', WebSearch: 'globe',
1051
+ };
1052
+ const iconName = toolIcons[progress.lastToolName] || 'sparkle';
1053
+
1054
+ const toolsBadge = S.agent_progress.tools_badge.replace('{count}', progress.toolCount);
1055
+ const durBadge = formatDuration(progress.durationMs);
1056
+
1057
+ const stalledClass = progress.stalled ? ' task-progress-stalled' : '';
1058
+ const stalledText = progress.stalled
1059
+ ? `<span class="task-progress-stall-warn">${S.agent_progress.stalled.replace('{duration}', this._formatStallDuration(progress.lastUpdate))}</span>`
1060
+ : '';
1061
+
1062
+ el.className = `task-progress${stalledClass}`;
1063
+ el.innerHTML = `<span class="task-progress-pulse"></span>`
1064
+ + `<span class="task-progress-desc">${escapeHtml(progress.description)}</span>`
1065
+ + `<span class="task-progress-badge">${toolsBadge}</span>`
1066
+ + (durBadge ? `<span class="task-progress-badge">${durBadge}</span>` : '')
1067
+ + stalledText;
1068
+
1069
+ // Also update the header badge to show "Running" with tool count
1070
+ const badge = taskBlock.querySelector('.task-block-badge');
1071
+ if (badge && badge.classList.contains('task-block-badge-pending')) {
1072
+ badge.textContent = progress.stalled ? 'Stalled' : 'Running...';
1073
+ if (progress.stalled) {
1074
+ badge.className = 'task-block-badge task-block-badge-warn';
1075
+ }
1076
+ }
1077
+
1078
+ // Update usage badges in header (live tokens + duration)
1079
+ const header = taskBlock.querySelector('.task-block-header');
1080
+ if (header) {
1081
+ header.querySelectorAll('.task-block-tokens, .task-block-duration').forEach(e => e.remove());
1082
+ const statusBadge = taskBlock.querySelector('.task-block-badge');
1083
+ const insertBefore = (node) => { if (statusBadge) statusBadge.before(node); else header.appendChild(node); };
1084
+
1085
+ if (durBadge) {
1086
+ const durEl = document.createElement('span');
1087
+ durEl.className = 'task-block-duration';
1088
+ durEl.textContent = durBadge;
1089
+ insertBefore(durEl);
1090
+ }
1091
+ const tokText = formatTokensBadge(progress.totalTokens);
1092
+ if (tokText) {
1093
+ const tokEl = document.createElement('span');
1094
+ tokEl.className = 'task-block-tokens';
1095
+ tokEl.textContent = tokText;
1096
+ insertBefore(tokEl);
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ _formatStallDuration(lastUpdate) {
1102
+ const elapsed = Math.floor((Date.now() - lastUpdate) / 1000);
1103
+ if (elapsed < 60) return `${elapsed}s`;
1104
+ return `${Math.floor(elapsed / 60)}m`;
1105
+ }
1106
+
1107
+ /**
1108
+ * Parse Read tool output to extract line numbers and content
1109
+ * Format: " 123→\tcontent" or " 123→content"
1110
+ */
1111
+ parseReadOutput(output) {
1112
+ const lines = [];
1113
+ let isTruncated = false;
1114
+ let firstLine = null;
1115
+ let lastLine = null;
1116
+
1117
+ if (!output) return { lines, isTruncated, firstLine, lastLine };
1118
+
1119
+ // Check for truncation markers
1120
+ if (output.includes('(truncated') || output.includes('...truncated')) {
1121
+ isTruncated = true;
1122
+ }
1123
+
1124
+ // Parse lines: Claude CLI uses either " NUMBER→CONTENT" (older) or "NUMBER\tCONTENT" (current)
1125
+ const linePattern = /^\s*(\d+)(?:→|\t)(.*)$/gm;
1126
+ let match;
1127
+ while ((match = linePattern.exec(output)) !== null) {
1128
+ const num = parseInt(match[1], 10);
1129
+ const content = match[2].replace(/^\t/, ''); // Remove leading tab (arrow-format may have → then tab)
1130
+ lines.push({ num, content });
1131
+
1132
+ if (firstLine === null || num < firstLine) firstLine = num;
1133
+ if (lastLine === null || num > lastLine) lastLine = num;
1134
+ }
1135
+
1136
+ return { lines, isTruncated, firstLine, lastLine };
1137
+ }
1138
+
1139
+ /**
1140
+ * Render a nice todo list for TodoWrite tool
1141
+ */
1142
+ truncateOutput(text, maxLen = this.maxOutputLength) {
1143
+ // Handle non-string outputs (e.g., image arrays from screenshots)
1144
+ if (Array.isArray(text)) {
1145
+ // Check if it's image content
1146
+ const hasImage = text.some(item => item?.type === 'image');
1147
+ if (hasImage) {
1148
+ return '[Image content]';
1149
+ }
1150
+ // Otherwise stringify
1151
+ text = JSON.stringify(text);
1152
+ } else if (typeof text === 'object' && text !== null) {
1153
+ text = JSON.stringify(text);
1154
+ }
1155
+
1156
+ const trimmed = (text || '').trim();
1157
+ if (trimmed.length > maxLen) {
1158
+ return trimmed.slice(0, maxLen);
1159
+ }
1160
+ return trimmed;
1161
+ }
1162
+
1163
+ /**
1164
+ * Update an existing tool result in the DOM
1165
+ * @returns {boolean} true if element was found and updated, false otherwise
1166
+ */
1167
+ updateToolResult(msg) {
1168
+ const toolBlock = $(`#tool-${msg.id}`);
1169
+ if (!toolBlock) {
1170
+ console.warn(`[ToolRenderer] Tool block not found: #tool-${msg.id} (${msg.toolName})`);
1171
+ return false;
1172
+ }
1173
+
1174
+ // Detect context for collapse mode (normal/thinking/agent)
1175
+ const isThinking = !!toolBlock.closest('.thinking-tool-card');
1176
+ const isAgent = !!toolBlock.closest('.task-block-children');
1177
+ const context = isAgent ? 'agent' : isThinking ? 'thinking' : 'normal';
1178
+ const toolType = msg?.toolName ? getToolCategory(msg.toolName) : 'execute';
1179
+ const mode = getToolCollapseMode(context, toolType);
1180
+ const expandOpts = { defaultExpanded: mode === 'expanded' };
1181
+
1182
+ // Handle Bash/Shell blocks - re-render completely since structure changes
1183
+ if (toolBlock.classList.contains('bash-block')) {
1184
+ const newHtml = this.renderBashBlock(msg, expandOpts);
1185
+ toolBlock.outerHTML = newHtml;
1186
+ // Start tracking if this is a background task
1187
+ const newEl = $(`#tool-${msg.id}`);
1188
+ if (newEl?.dataset?.taskId) {
1189
+ bgTaskTracker.track(newEl.dataset.taskId, msg.toolInput?.command);
1190
+ }
1191
+ return true;
1192
+ }
1193
+
1194
+ // Handle Read blocks - re-render completely
1195
+ if (toolBlock.classList.contains('read-block')) {
1196
+ const newHtml = this.renderReadBlock(msg, expandOpts);
1197
+ toolBlock.outerHTML = newHtml;
1198
+ const newEl = $(`#tool-${msg.id}`);
1199
+ if (newEl) this.processReadExcalidraw(newEl);
1200
+ return true;
1201
+ }
1202
+
1203
+ // Handle Grep blocks - re-render completely
1204
+ if (toolBlock.classList.contains('grep-block')) {
1205
+ const newHtml = this.renderGrepBlock(msg, expandOpts);
1206
+ toolBlock.outerHTML = newHtml;
1207
+ return true;
1208
+ }
1209
+
1210
+ // Handle Glob blocks - re-render completely
1211
+ if (toolBlock.classList.contains('glob-block')) {
1212
+ const newHtml = this.renderGlobBlock(msg, expandOpts);
1213
+ toolBlock.outerHTML = newHtml;
1214
+ return true;
1215
+ }
1216
+
1217
+ // Handle Write blocks - re-render completely
1218
+ if (toolBlock.classList.contains('write-block')) {
1219
+ const newHtml = this.renderWriteBlock(msg, expandOpts);
1220
+ toolBlock.outerHTML = newHtml;
1221
+ // Process chart/excalidraw rendering after DOM replacement
1222
+ const newEl = $(`#tool-${msg.id}`);
1223
+ if (newEl) { this.processWriteCharts(newEl); this.processWriteExcalidraw(newEl); }
1224
+ return true;
1225
+ }
1226
+
1227
+ // Handle WebFetch/WebSearch blocks - re-render completely
1228
+ if (toolBlock.classList.contains('webfetch-block')) {
1229
+ const newHtml = this.renderWebFetchBlock(msg, expandOpts);
1230
+ toolBlock.outerHTML = newHtml;
1231
+ return true;
1232
+ }
1233
+
1234
+ // Handle EnterPlanMode blocks - re-render completely
1235
+ if (toolBlock.classList.contains('plan-mode-block')) {
1236
+ toolBlock.outerHTML = this.renderEnterPlanBlock(msg);
1237
+ return true;
1238
+ }
1239
+
1240
+ // Handle Task blocks - update in place to preserve children
1241
+ if (toolBlock.classList.contains('task-block')) {
1242
+ this._updateTaskBlockResult(toolBlock, msg);
1243
+ return true;
1244
+ }
1245
+
1246
+ // Handle Skill blocks - re-render completely
1247
+ if (toolBlock.classList.contains('skill-block')) {
1248
+ toolBlock.outerHTML = this.renderSkillBlock(msg);
1249
+ return true;
1250
+ }
1251
+
1252
+ // Handle TaskStop blocks - re-render (check before .tob since tob-stop also has .tob)
1253
+ if (toolBlock.classList.contains('tob-stop')) {
1254
+ toolBlock.outerHTML = this.renderTaskStopBlock(msg);
1255
+ return true;
1256
+ }
1257
+
1258
+ // Handle TaskOutput blocks - re-render completely
1259
+ if (toolBlock.classList.contains('tob')) {
1260
+ toolBlock.outerHTML = this.renderTaskOutputBlock(msg);
1261
+ return true;
1262
+ }
1263
+
1264
+ // Handle Edit tool diff blocks - re-render completely for correct line numbers
1265
+ if (toolBlock.classList.contains('edit-tool-block')) {
1266
+ const diffHtml = this.renderEditDiff(msg.toolInput, msg.toolOutput, msg.toolId, msg.startLine);
1267
+ if (diffHtml) {
1268
+ const statusClass = msg.toolError ? 'edit-status-error' : 'edit-status-success';
1269
+ const statusText = msg.toolError ? 'Failed: ' + escapeHtml(msg.toolError) : 'Applied';
1270
+ toolBlock.innerHTML = `${diffHtml}\n<div class="edit-status ${statusClass}">${statusText}</div>`;
1271
+ } else if (msg.toolError) {
1272
+ const editStatus = toolBlock.querySelector('.edit-status');
1273
+ if (editStatus) {
1274
+ editStatus.className = 'edit-status edit-status-error';
1275
+ editStatus.textContent = 'Failed: ' + msg.toolError;
1276
+ }
1277
+ }
1278
+ return true;
1279
+ }
1280
+
1281
+ // Handle regular tool blocks
1282
+ const outputEl = toolBlock.querySelector('.tool-output');
1283
+ if (outputEl) {
1284
+ outputEl.className = `tool-output ${msg.toolError ? 'error' : ''}`;
1285
+ outputEl.textContent = this.truncateOutput(msg.toolOutput || msg.toolError || S.tool_renderer.badges.done);
1286
+ }
1287
+ return true;
1288
+ }
1289
+
1290
+ /**
1291
+ * Process chart rendering in Write tool blocks
1292
+ * Finds .write-chart-pending elements, fetches SVG from API, replaces loading state
1293
+ */
1294
+ processWriteCharts(container) {
1295
+ if (!container) return;
1296
+ const pending = container.querySelectorAll('.write-chart-pending');
1297
+ if (pending.length === 0) return;
1298
+
1299
+ pending.forEach(el => {
1300
+ el.classList.remove('write-chart-pending');
1301
+
1302
+ const encoded = el.dataset.chartJson;
1303
+ if (!encoded) return;
1304
+
1305
+ let jsonText;
1306
+ try {
1307
+ jsonText = decodeURIComponent(escape(atob(encoded)));
1308
+ } catch {
1309
+ el.innerHTML = `<div class="chart-inline-error">${S.tool_renderer.errors.decode_chart}</div>`;
1310
+ return;
1311
+ }
1312
+
1313
+ if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
1314
+ el.innerHTML = `<div class="chart-inline-error">${S.tool_renderer.errors.chart_disabled}</div>`;
1315
+ return;
1316
+ }
1317
+
1318
+ fetch('/api/chart/render', {
1319
+ method: 'POST',
1320
+ headers: { 'Content-Type': 'application/json' },
1321
+ body: jsonText,
1322
+ })
1323
+ .then(resp => {
1324
+ if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
1325
+ return resp.text();
1326
+ })
1327
+ .then(svg => {
1328
+ el.innerHTML = `<div class="chart-inline-rendered"><div class="chart-inline-svg">${sanitizeSvg(svg)}</div></div>`;
1329
+ })
1330
+ .catch(err => {
1331
+ console.error('Write chart render error:', err);
1332
+ el.innerHTML = `<div class="chart-inline-error"><span>Chart render error: ${escapeHtml(err.message)}</span></div>`;
1333
+ });
1334
+ });
1335
+ }
1336
+
1337
+ /**
1338
+ * Post-DOM rendering for excalidraw Write blocks — sends JSON to /api/excalidraw/render
1339
+ */
1340
+ processWriteExcalidraw(container) {
1341
+ if (!container) return;
1342
+ const pending = container.querySelectorAll('.write-excalidraw-pending');
1343
+ if (pending.length === 0) return;
1344
+
1345
+ pending.forEach(el => {
1346
+ el.classList.remove('write-excalidraw-pending');
1347
+
1348
+ const encoded = el.dataset.excalidrawJson;
1349
+ if (!encoded) return;
1350
+
1351
+ let jsonText;
1352
+ try {
1353
+ jsonText = decodeURIComponent(escape(atob(encoded)));
1354
+ } catch {
1355
+ el.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.decode_diagram}</div>`;
1356
+ return;
1357
+ }
1358
+
1359
+ if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
1360
+ el.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.diagram_disabled}</div>`;
1361
+ return;
1362
+ }
1363
+
1364
+ fetch('/api/excalidraw/render?dark=true', {
1365
+ method: 'POST',
1366
+ headers: { 'Content-Type': 'application/json' },
1367
+ body: jsonText,
1368
+ })
1369
+ .then(resp => {
1370
+ if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
1371
+ return resp.text();
1372
+ })
1373
+ .then(svg => {
1374
+ const fp = el.closest('.write-excalidraw')?.dataset?.filePath || '';
1375
+ el.innerHTML = `<div class="excalidraw-inline-rendered"><div class="excalidraw-inline-svg excalidraw-clickable" data-act="preview-file" data-file="${escapeAttr(fp)}">${sanitizeSvg(svg)}</div></div>`;
1376
+ })
1377
+ .catch(err => {
1378
+ console.error('Write excalidraw render error:', err);
1379
+ el.innerHTML = `<div class="excalidraw-inline-error"><span>Diagram render error: ${escapeHtml(err.message)}</span></div>`;
1380
+ });
1381
+ });
1382
+ }
1383
+
1384
+ /**
1385
+ * Post-DOM rendering for excalidraw Read blocks — fetches SVG from /api/file-raw
1386
+ */
1387
+ processReadExcalidraw(container) {
1388
+ if (!container) return;
1389
+ const targets = container.querySelectorAll('.read-excalidraw-loading');
1390
+ if (targets.length === 0) return;
1391
+
1392
+ targets.forEach(loading => {
1393
+ const svgDiv = loading.nextElementSibling;
1394
+ if (!svgDiv?.dataset?.src) return;
1395
+
1396
+ if (window.INSTANCE_CONFIG?.renderers_enabled !== true) {
1397
+ loading.innerHTML = `<div class="excalidraw-inline-error">${S.tool_renderer.errors.diagram_disabled}</div>`;
1398
+ return;
1399
+ }
1400
+
1401
+ fetch(svgDiv.dataset.src)
1402
+ .then(resp => {
1403
+ if (!resp.ok) throw new Error(`Render failed: ${resp.status}`);
1404
+ return resp.text();
1405
+ })
1406
+ .then(svg => {
1407
+ const fp = svgDiv.closest('.read-excalidraw')?.dataset?.filePath || '';
1408
+ svgDiv.innerHTML = `<div class="excalidraw-inline-svg excalidraw-clickable" data-act="preview-file" data-file="${escapeAttr(fp)}">${sanitizeSvg(svg)}</div>`;
1409
+ svgDiv.style.display = '';
1410
+ loading.remove();
1411
+ })
1412
+ .catch(err => {
1413
+ console.error('Read excalidraw render error:', err);
1414
+ loading.innerHTML = `<div class="excalidraw-inline-error"><span>Diagram render error: ${escapeHtml(err.message)}</span></div>`;
1415
+ });
1416
+ });
1417
+ }
1418
+
1419
+ /**
1420
+ * Toggle tool block collapsed state
1421
+ */
1422
+ toggleTool(msgId) {
1423
+ const toolBlock = $(`#tool-${msgId}`)?.querySelector('.tool-block') || $(`#tool-${msgId}`);
1424
+ if (toolBlock) {
1425
+ toolBlock.classList.toggle('collapsed');
1426
+ }
1427
+ }
1428
+
1429
+ /**
1430
+ * Render tool output with expansion handling
1431
+ */
1432
+ renderToolOutput(content, isCompleted = false, threshold = 5) {
1433
+ if (!content) return isCompleted ? S.tool_renderer.badges.done : '';
1434
+
1435
+ const lines = content.split('\n');
1436
+ if (lines.length <= threshold) {
1437
+ return escapeHtml(content);
1438
+ }
1439
+
1440
+ // Show first few lines with expand option
1441
+ const preview = lines.slice(0, threshold).join('\n');
1442
+ return `<div class="tool-output-expandable">
1443
+ <div class="tool-output-preview">${escapeHtml(preview)}</div>
1444
+ <div class="tool-output-full hidden">${escapeHtml(content)}</div>
1445
+ <button class="tool-output-expand" data-act="toggle-tool-output" data-block=".tool-output-expandable">Show more</button>
1446
+ </div>`;
1447
+ }
1448
+
1449
+ /**
1450
+ * Render a compact styled tool block for thinking messages.
1451
+ * Each tool type gets a distinct visual treatment.
1452
+ * @param {Object} tool - Tool object with toolName, toolInput, toolOutput, toolId, toolCompleted
1453
+ * @returns {string} HTML for the thinking tool block
1454
+ */
1455
+ _parseBashResult(cmd, output) {
1456
+ const lines = output.split('\n').filter(l => l.trim());
1457
+
1458
+ // ls command - show file count or file names
1459
+ if (cmd.match(/^ls\b/)) {
1460
+ if (lines.length <= 3) {
1461
+ // Few files - show names
1462
+ const names = lines.map(l => l.split(/\s+/).pop()).filter(Boolean);
1463
+ return `<span class="ta-result">${names.join(', ')}</span>`;
1464
+ }
1465
+ return `<span class="ta-result">${lines.length} items</span>`;
1466
+ }
1467
+
1468
+ // git status - summarize
1469
+ if (cmd.match(/^git\s+status/)) {
1470
+ const modified = (output.match(/modified:/g) || []).length;
1471
+ const added = (output.match(/new file:/g) || []).length;
1472
+ const deleted = (output.match(/deleted:/g) || []).length;
1473
+ if (modified + added + deleted === 0) {
1474
+ return '<span class="ta-muted">clean</span>';
1475
+ }
1476
+ const parts = [];
1477
+ if (modified) parts.push(`${modified} modified`);
1478
+ if (added) parts.push(`${added} new`);
1479
+ if (deleted) parts.push(`${deleted} deleted`);
1480
+ return `<span class="ta-result">${parts.join(', ')}</span>`;
1481
+ }
1482
+
1483
+ // git diff --stat
1484
+ if (cmd.match(/^git\s+diff/)) {
1485
+ const statMatch = output.match(/(\d+) files? changed/);
1486
+ if (statMatch) {
1487
+ return `<span class="ta-result">${statMatch[0]}</span>`;
1488
+ }
1489
+ }
1490
+
1491
+ // Default: line count
1492
+ return `<span class="ta-result">${lines.length} line${lines.length !== 1 ? 's' : ''}</span>`;
1493
+ }
1494
+
1495
+ /** Bash/Shell - terminal style with command and output preview */
1496
+ }
1497
+
1498
+
1499
+ // Mix block-mode and thinking-mode methods into ToolRenderer.prototype.
1500
+ Object.assign(ToolRenderer.prototype, blockMethods, thinkingMethods);