create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Public API on window.MR:
14
14
  * - formatMsgText(text) — markdown + tool-badge HTML
15
- * - classifyMessage(m, stripped, isToolOnly) — 'key'|'normal'|'self-thought'|'summary'
15
+ * - classifyMessage(m, stripped, isToolOnly) — 'key'|'normal'|'self-thought'|'summary'|'walle-error'
16
16
  * - renderReviewMsg(m, i, msgType?) — Review row HTML string
17
17
  * - renderSelfThoughtMsg(m, i, stripped) — single-self-thought collapsed group HTML
18
18
  * - renderSelfThoughtItem(m, i, stripped) — inner item inside a multi-step thought-group
@@ -24,6 +24,7 @@
24
24
  * - renderConversationEvent(evt) — Conversation row DOM element
25
25
  * - createConversationTurn(evt, opts) — Conversation prompt-turn DOM
26
26
  * - refreshConversationActivityGroup(group) — recompute grouped tool UI
27
+ * - refreshConversationSystemGroup(group) — recompute grouped system UI
27
28
  *
28
29
  * Depends on global escHtml (defined in index.html). Since the renderer
29
30
  * functions are only invoked AFTER the page's body scripts run, escHtml
@@ -39,6 +40,219 @@
39
40
  return '/api/images/file/' + encodeURIComponent(match[1]);
40
41
  }
41
42
 
43
+ const _IMAGE_REF_EXT_RE = /\.(?:png|jpe?g|gif|webp|svg|heic|heif|bmp|tiff?)(?:[?#].*)?$/i;
44
+ const _IMAGE_ATTACHMENT_SAFE_ATTRS = ['href', 'src', 'alt', 'title', 'target', 'rel', 'loading', 'aria-label'];
45
+ const _SAFE_INLINE_IMAGE_DATA_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,[a-z0-9+/=\s]+$/i;
46
+
47
+ function _stripWrappingQuotes(value) {
48
+ let text = String(value || '').trim();
49
+ if (!text) return '';
50
+ if (text[0] === '<' && text[text.length - 1] === '>') text = text.slice(1, -1).trim();
51
+ const first = text[0];
52
+ const last = text[text.length - 1];
53
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) text = text.slice(1, -1);
54
+ return text.replace(/\\"/g, '"').replace(/\\'/g, "'").trim();
55
+ }
56
+
57
+ function _basenameFromReference(value) {
58
+ let text = _stripWrappingQuotes(value);
59
+ if (!text) return '';
60
+ try {
61
+ if (/^https?:\/\//i.test(text) || /^file:\/\//i.test(text)) text = new URL(text).pathname || text;
62
+ } catch {}
63
+ try { text = decodeURIComponent(text); } catch {}
64
+ return String(text || '').replace(/[?#].*$/g, '').split(/[\\/]/).filter(Boolean).pop() || '';
65
+ }
66
+
67
+ function _imageApiUrlForReference(value) {
68
+ const raw = _stripWrappingQuotes(value);
69
+ if (!raw) return '';
70
+ if (/^data:image\//i.test(raw)) return _SAFE_INLINE_IMAGE_DATA_RE.test(raw) ? raw : '';
71
+ if (/^\/api\/images\/file\//i.test(raw)) return raw;
72
+ try {
73
+ if (/^https?:\/\//i.test(raw)) {
74
+ const u = new URL(raw);
75
+ if (/\/api\/images\/file\//i.test(u.pathname)) return u.pathname + u.search;
76
+ }
77
+ if (/^file:\/\//i.test(raw)) {
78
+ const u = new URL(raw);
79
+ const api = _imageApiUrlForLocalPath(u.pathname || '');
80
+ if (api) return api;
81
+ }
82
+ } catch {}
83
+ const api = _imageApiUrlForLocalPath(raw);
84
+ if (api) return api;
85
+ const basename = _basenameFromReference(raw);
86
+ if (basename && _IMAGE_REF_EXT_RE.test(basename)) return '/api/images/file/' + encodeURIComponent(basename);
87
+ return '';
88
+ }
89
+
90
+ function _imageReferenceKey(value) {
91
+ const api = _imageApiUrlForReference(value);
92
+ if (api) return api.replace(/[?#].*$/g, '').toLowerCase();
93
+ return _stripWrappingQuotes(value).replace(/[?#].*$/g, '').toLowerCase();
94
+ }
95
+
96
+ function _labelNumber(label) {
97
+ const m = String(label || '').match(/\[Image #(\d+)\]/i);
98
+ return m ? Number(m[1]) : 0;
99
+ }
100
+
101
+ function _nextImageLabel(existing) {
102
+ let max = 0;
103
+ for (const item of existing || []) max = Math.max(max, _labelNumber(item && item.label));
104
+ return '[Image #' + (max + 1) + ']';
105
+ }
106
+
107
+ function _splitImageReferenceList(value) {
108
+ const text = String(value || '');
109
+ const refs = [];
110
+ const quoted = /"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'/g;
111
+ let match;
112
+ while ((match = quoted.exec(text))) refs.push(match[1] || match[2] || '');
113
+ if (refs.length) return refs;
114
+ return text.split(',').map(part => part.trim()).filter(Boolean);
115
+ }
116
+
117
+ function _attachmentReferenceValue(item) {
118
+ if (typeof item === 'string') return item;
119
+ if (!item || typeof item !== 'object') return '';
120
+ return item.url || item.previewUrl || item.preview_url || item.data || item.path || item.file_path || item.filename || item.name || '';
121
+ }
122
+
123
+ function _attachmentLooksImage(item) {
124
+ if (typeof item === 'string') return !!_imageApiUrlForReference(item);
125
+ if (!item || typeof item !== 'object') return false;
126
+ const kind = String(item.kind || item.type || item.mediaType || item.mimeType || item.mime_type || '').toLowerCase();
127
+ if (kind.includes('image')) return true;
128
+ return _IMAGE_REF_EXT_RE.test(String(_attachmentReferenceValue(item) || ''));
129
+ }
130
+
131
+ function _messagePlainText(message) {
132
+ const direct = message.text ?? message.message ?? message.content ?? '';
133
+ if (typeof direct === 'string') return direct;
134
+ if (Array.isArray(direct)) {
135
+ return direct.map((part) => {
136
+ if (typeof part === 'string') return part;
137
+ if (!part || typeof part !== 'object') return '';
138
+ return part.text || part.content || part.message || '';
139
+ }).filter(Boolean).join('\n');
140
+ }
141
+ if (direct && typeof direct === 'object') return direct.text || direct.message || '';
142
+ return '';
143
+ }
144
+
145
+ function _addImageAttachment(out, seen, value, label, source) {
146
+ const ref = _stripWrappingQuotes(value);
147
+ const src = _imageApiUrlForReference(ref);
148
+ if (!src) return null;
149
+ const key = _imageReferenceKey(ref) || src;
150
+ if (seen.has(key)) return null;
151
+ seen.add(key);
152
+ const item = {
153
+ label: label || _nextImageLabel(out),
154
+ src,
155
+ href: src,
156
+ filename: _basenameFromReference(ref) || _basenameFromReference(src),
157
+ source: source || 'text',
158
+ };
159
+ out.push(item);
160
+ return item;
161
+ }
162
+
163
+ MR.extractImageAttachments = function (messageOrText) {
164
+ const message = messageOrText && typeof messageOrText === 'object' ? messageOrText : { text: messageOrText };
165
+ const text = _messagePlainText(message);
166
+ const out = [];
167
+ const seen = new Set();
168
+
169
+ const structuredSources = [
170
+ message.attachments,
171
+ message.images,
172
+ message.imageRefs,
173
+ message.image_refs,
174
+ message.data && message.data.attachments,
175
+ message.data && message.data.images,
176
+ message.data && message.data.imageRefs,
177
+ message.data && message.data.image_refs,
178
+ ];
179
+ for (const rawList of structuredSources) {
180
+ let list = rawList;
181
+ if (typeof list === 'string') {
182
+ try { list = JSON.parse(list); } catch { list = []; }
183
+ }
184
+ if (!Array.isArray(list)) continue;
185
+ for (let idx = 0; idx < list.length; idx += 1) {
186
+ const item = list[idx];
187
+ if (!_attachmentLooksImage(item)) continue;
188
+ _addImageAttachment(out, seen, _attachmentReferenceValue(item), item.label || item.token || '', 'structured');
189
+ }
190
+ }
191
+
192
+ const contentBlocks = Array.isArray(message.contentBlocks)
193
+ ? message.contentBlocks
194
+ : (Array.isArray(message.data && message.data.contentBlocks) ? message.data.contentBlocks : []);
195
+ for (let idx = 0; idx < contentBlocks.length; idx += 1) {
196
+ const block = contentBlocks[idx] || {};
197
+ if (!_attachmentLooksImage(block) && !/image/i.test(String(block.type || ''))) continue;
198
+ _addImageAttachment(out, seen, _attachmentReferenceValue(block), block.label || '', 'content-block');
199
+ }
200
+
201
+ text.replace(/\[(Image #\d+)\s*:\s*("((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\]]+))\]/gi, (_full, label, _wrapped, dq, sq, bare) => {
202
+ _addImageAttachment(out, seen, dq || sq || bare || '', '[' + label.replace(/^\[|\]$/g, '') + ']', 'text-token');
203
+ return _full;
204
+ });
205
+
206
+ text.replace(/\[(Attached images?)\s*:\s*([^\]]+)\]/gi, (_full, label, body) => {
207
+ const refs = _splitImageReferenceList(body);
208
+ for (const ref of refs) _addImageAttachment(out, seen, ref, '', label.toLowerCase());
209
+ return _full;
210
+ });
211
+
212
+ return out;
213
+ };
214
+
215
+ MR.stripImageAttachmentMetadata = function (text) {
216
+ let out = String(text || '');
217
+ out = out.replace(/\[(Image #\d+)\s*:\s*("((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^\]]+))\]/gi, (_full, label) => '[' + label.replace(/^\[|\]$/g, '') + ']');
218
+ out = out.replace(/[ \t]*\[(Attached images?)\s*:\s*[^\]]+\][ \t]*/gi, ' ');
219
+ out = out.replace(/[ \t]*\[Image:\s*source:\s*(?:"[^"]+"|'[^']+'|[^\]]+)\][ \t]*/gi, ' ');
220
+ out = out.replace(/[ \t]*\[Image:\s*original\s+\d+(?:\.\d+)?x\d+(?:\.\d+)?,\s*displayed at\s+\d+(?:\.\d+)?x\d+(?:\.\d+)?\.\s*Multiply coordinates by\s+[0-9.]+\s+to map to original image\.?\][ \t]*/gi, ' ');
221
+ return out.replace(/[ \t]{2,}/g, ' ').replace(/[ \t]+(\r?\n)/g, '$1').replace(/(\r?\n)[ \t]+/g, '$1').trim();
222
+ };
223
+
224
+ MR.renderImageAttachmentsHtml = function (messageOrAttachments) {
225
+ const attachments = Array.isArray(messageOrAttachments)
226
+ ? messageOrAttachments
227
+ : MR.extractImageAttachments(messageOrAttachments);
228
+ if (!attachments.length) return '';
229
+ return '<div class="msg-attachment-strip" aria-label="Image attachments">'
230
+ + attachments.map((att, idx) => {
231
+ const label = att.label || '[Image #' + (idx + 1) + ']';
232
+ const name = att.filename || label;
233
+ return '<a class="msg-image-attachment" href="' + escHtml(att.href || att.src) + '" target="_blank" rel="noopener noreferrer" title="' + escHtml(name) + '">'
234
+ + '<img src="' + escHtml(att.src) + '" alt="' + escHtml(label + (name && name !== label ? ' ' + name : '')) + '" loading="lazy">'
235
+ + '<span>' + escHtml(label) + '</span>'
236
+ + '</a>';
237
+ }).join('')
238
+ + '</div>';
239
+ };
240
+
241
+ function _messageTextForDisplay(text) {
242
+ return MR.stripImageAttachmentMetadata(text);
243
+ }
244
+
245
+ function _appendReviewAttachmentsHtml(message) {
246
+ const html = MR.renderImageAttachmentsHtml(message);
247
+ return html ? html : '';
248
+ }
249
+
250
+ function _formatDisplayTextHtml(text, emptyHtml) {
251
+ const displayText = _messageTextForDisplay(text);
252
+ if (displayText) return MR.formatMsgText(displayText);
253
+ return emptyHtml || '';
254
+ }
255
+
42
256
  function _normalizeMarkdownImageSources(text) {
43
257
  return String(text || '').replace(/!\[([^\]]*)\]\(([^)\s]+|<[^>]+>)\)/g, (all, alt, src) => {
44
258
  const apiUrl = _imageApiUrlForLocalPath(src);
@@ -609,12 +823,521 @@
609
823
  return "if(event.key==='Enter'||event.key===' '){event.preventDefault();this.classList.toggle('expanded')}";
610
824
  }
611
825
 
826
+ // ------------------------------------------------------------------
827
+ // Structured-capture metadata (parse-time capture of tool calls/results,
828
+ // reasoning, patches, compaction — see lib/structured-capture.js).
829
+ // ------------------------------------------------------------------
830
+
831
+ // Normalized reader: messages carry `metadata`, conversation events carry
832
+ // `data.metadata`. Returns the metadata object only when it is a structured
833
+ // v1+ capture (has kind + version) — legacy rows return null and take the
834
+ // text pipeline unchanged.
835
+ MR.messageMeta = function (mOrEvt) {
836
+ if (!mOrEvt || typeof mOrEvt !== 'object') return null;
837
+ const candidates = [mOrEvt.metadata, mOrEvt.data && mOrEvt.data.metadata];
838
+ for (const meta of candidates) {
839
+ if (meta && typeof meta === 'object' && typeof meta.kind === 'string' && Number(meta.v) >= 1) return meta;
840
+ }
841
+ return null;
842
+ };
843
+
844
+ // Kinds the v1 schema emits. A kind outside this set is format drift —
845
+ // rendered as an inspectable raw-JSON card, never silently dropped.
846
+ const _KNOWN_META_KINDS = new Set([
847
+ 'reasoning', 'tool_call', 'tool_result', 'shell', 'patch',
848
+ 'web_search', 'compact_boundary', 'compact_summary',
849
+ ]);
850
+
851
+ // Build the HTML-string card for a structured message, or '' when the kind
852
+ // has no card yet (it then falls through to the legacy text pipeline).
853
+ // BOTH render paths use this — renderReviewMsg returns the string and
854
+ // renderConversationEvent hydrates the same string via _cardElFromHtml —
855
+ // so Review/Conversation parity holds by construction.
856
+ MR.renderMetaCardHtml = function (m, i) {
857
+ const meta = MR.messageMeta(m);
858
+ if (!meta) return '';
859
+ if (meta.kind === 'unknown' || !_KNOWN_META_KINDS.has(meta.kind)) {
860
+ return MR.renderUnknownKindHtml(m, i, meta);
861
+ }
862
+ if (_TOOL_CARD_KINDS.has(meta.kind)) return MR.renderToolCardHtml(m, null, i);
863
+ if (meta.kind === 'tool_result') return MR.renderToolCardHtml(null, m, i);
864
+ if (meta.kind === 'reasoning') return MR.renderThinkingHtml(m, i, meta);
865
+ if (meta.kind === 'compact_boundary') return MR.renderCompactBoundaryHtml(m, i, meta);
866
+ if (meta.kind === 'compact_summary') return MR.renderCompactSummaryHtml(m, i, meta);
867
+ return '';
868
+ };
869
+
870
+ // ------------------------------------------------------------------
871
+ // Reasoning (thinking) pills + compaction divider/summary.
872
+ // ------------------------------------------------------------------
873
+ function _fmtTokens(n) {
874
+ const value = Number(n);
875
+ if (!Number.isFinite(value) || value <= 0) return '';
876
+ if (value >= 1_000_000) return (value / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
877
+ if (value >= 1000) return Math.round(value / 1000) + 'k';
878
+ return String(value);
879
+ }
880
+ MR._fmtTokens = _fmtTokens;
881
+
882
+ // Per-turn token accounting from assistant message.usage metadata (captured
883
+ // at parse time). The chip counts what the turn CONSUMED: input + output +
884
+ // cache writes (cache reads shown in the breakdown title only).
885
+ function _usageFromMessage(m) {
886
+ const usage = m && m.metadata && typeof m.metadata === 'object' ? m.metadata.usage : null;
887
+ return usage && typeof usage === 'object' ? usage : null;
888
+ }
889
+ MR.usageTokensTotal = function (usage) {
890
+ if (!usage || typeof usage !== 'object') return 0;
891
+ return (Number(usage.input_tokens) || 0)
892
+ + (Number(usage.output_tokens) || 0)
893
+ + (Number(usage.cache_creation_input_tokens) || 0);
894
+ };
895
+ MR.usageTitle = function (usage) {
896
+ if (!usage || typeof usage !== 'object') return '';
897
+ const parts = [];
898
+ if (usage.input_tokens) parts.push('in ' + _fmtTokens(usage.input_tokens));
899
+ if (usage.output_tokens) parts.push('out ' + _fmtTokens(usage.output_tokens));
900
+ if (usage.cache_creation_input_tokens) parts.push('cache write ' + _fmtTokens(usage.cache_creation_input_tokens));
901
+ if (usage.cache_read_input_tokens) parts.push('cache read ' + _fmtTokens(usage.cache_read_input_tokens));
902
+ return parts.join(' · ');
903
+ };
904
+ MR._usageFromMessage = _usageFromMessage;
905
+
906
+ MR.renderThinkingHtml = function (m, i, meta) {
907
+ const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
908
+ if (meta.encrypted) {
909
+ return '<div class="review-msg thinking-block encrypted" data-msg-idx="' + escHtml(String(i)) + '">'
910
+ + '<div class="msg-header">'
911
+ + '<span class="msg-role">Thinking</span>'
912
+ + '<span class="thought-preview">(encrypted by the provider)</span>'
913
+ + '<span class="msg-time">' + escHtml(time) + '</span>'
914
+ + '</div>'
915
+ + '</div>';
916
+ }
917
+ const body = _textAfterMarkerLine(m.text);
918
+ const firstLine = body.split('\n').find(l => l.trim()) || '';
919
+ return '<div class="review-msg thinking-block"' + MR.rawJsonAttrHtml(m, meta)
920
+ + ' onclick="if(!MR.hasTextSelectionInside||!MR.hasTextSelectionInside(this)){this.classList.toggle(\'expanded\')}"'
921
+ + ' onkeydown="' + _toggleOnKeydownHandler() + '" role="button" tabindex="0" data-msg-idx="' + escHtml(String(i)) + '">'
922
+ + '<div class="msg-header">'
923
+ + '<span class="msg-role"><span class="thought-chevron">▶</span>Thinking</span>'
924
+ + '<span class="thought-preview">' + escHtml(_previewFromText(firstLine, 90)) + '</span>'
925
+ + MR.rawJsonButtonHtml()
926
+ + '<span class="msg-time">' + escHtml(time) + '</span>'
927
+ + '</div>'
928
+ + '<div class="msg-text" onclick="event.stopPropagation()">' + MR.formatMsgText(body) + '</div>'
929
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
930
+ + '</div>';
931
+ };
932
+
933
+ MR.renderCompactBoundaryHtml = function (m, i, meta) {
934
+ const parts = [];
935
+ if (meta.trigger) parts.push(meta.trigger);
936
+ const tokens = _fmtTokens(meta.preTokens);
937
+ if (tokens) parts.push(tokens + ' tokens');
938
+ const label = 'Context compacted' + (parts.length ? ' — ' + parts.join(', ') : '');
939
+ return '<div class="compact-divider" role="separator" data-msg-idx="' + escHtml(String(i)) + '">'
940
+ + '<span class="compact-divider-line"></span>'
941
+ + '<span class="compact-divider-label">' + escHtml(label) + '</span>'
942
+ + '<span class="compact-divider-line"></span>'
943
+ + '</div>';
944
+ };
945
+
946
+ MR.renderCompactSummaryHtml = function (m, i, meta) {
947
+ const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
948
+ const preview = _previewFromText(m.text, 90);
949
+ return '<div class="review-msg skill-body compact-summary"' + MR.rawJsonAttrHtml(m, meta)
950
+ + ' onclick="if(!MR.hasTextSelectionInside||!MR.hasTextSelectionInside(this)){this.classList.toggle(\'expanded\')}"'
951
+ + ' onkeydown="' + _toggleOnKeydownHandler() + '" role="button" tabindex="0" data-msg-idx="' + escHtml(String(i)) + '">'
952
+ + '<div class="msg-header">'
953
+ + '<span class="msg-role"><span class="thought-chevron">▶</span>Compacted context</span>'
954
+ + '<span class="thought-preview">' + escHtml(preview) + '</span>'
955
+ + MR.rawJsonButtonHtml()
956
+ + '<span class="msg-time">' + escHtml(time) + '</span>'
957
+ + '</div>'
958
+ + '<div class="msg-text" onclick="event.stopPropagation()">' + MR.formatMsgText(m.text) + '</div>'
959
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
960
+ + '</div>';
961
+ };
962
+
963
+ // ------------------------------------------------------------------
964
+ // Tool cards: one collapsible card per tool/shell/patch/web-search call,
965
+ // its result filled in (paired by metadata.callId at render time on the
966
+ // Review path, live via fillToolCardResult on the streaming path).
967
+ // ------------------------------------------------------------------
968
+ const _TOOL_CARD_KINDS = new Set(['tool_call', 'shell', 'patch', 'web_search']);
969
+
970
+ MR.prettifyToolName = function (name) {
971
+ const raw = String(name || '').trim();
972
+ const mcp = raw.match(/^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/);
973
+ if (mcp) return mcp[1] + ': ' + mcp[2];
974
+ return raw || 'tool';
975
+ };
976
+
977
+ function _toolCardTitle(meta) {
978
+ switch (meta.kind) {
979
+ case 'shell': return 'Shell';
980
+ case 'patch': return 'Patch';
981
+ case 'web_search': return 'Web search';
982
+ default: return MR.prettifyToolName(meta.tool);
983
+ }
984
+ }
985
+
986
+ function _toolCardPreview(meta) {
987
+ if (meta.kind === 'shell') return _previewFromText(meta.command || '', 90);
988
+ if (meta.kind === 'web_search') return _previewFromText(meta.query || '', 90);
989
+ if (meta.kind === 'patch') return _previewFromText((meta.files || []).join(', ') || meta.filePath || '', 90);
990
+ let args = String(meta.argsPreview || '');
991
+ try {
992
+ const parsed = JSON.parse(args);
993
+ if (parsed && typeof parsed === 'object') {
994
+ const salient = parsed.command || parsed.file_path || parsed.filePath || parsed.pattern || parsed.path || parsed.url || parsed.query;
995
+ if (salient != null) args = Array.isArray(salient) ? salient.join(' ') : String(salient);
996
+ }
997
+ } catch { /* keep raw */ }
998
+ return _previewFromText(args, 90);
999
+ }
1000
+
1001
+ // Patch/tool text fallbacks carry a marker first line ("[Patch] a.js",
1002
+ // "[Tool result: Bash]") — the body shows the content after it.
1003
+ function _textAfterMarkerLine(text) {
1004
+ const s = String(text || '');
1005
+ if (!/^\[[^\]]*\]/.test(s)) return s;
1006
+ const nl = s.indexOf('\n');
1007
+ return nl === -1 ? '' : s.slice(nl + 1);
1008
+ }
1009
+
1010
+ function _toolCardInputText(meta, m) {
1011
+ if (meta.kind === 'shell') return meta.command || '';
1012
+ if (meta.kind === 'web_search') return meta.query || '';
1013
+ if (meta.kind === 'patch') return _textAfterMarkerLine(m && m.text);
1014
+ let args = String(meta.argsPreview || '');
1015
+ if (!meta.truncated) {
1016
+ try { args = JSON.stringify(JSON.parse(args), null, 2); } catch { /* keep raw */ }
1017
+ }
1018
+ return args;
1019
+ }
1020
+
1021
+ // Patch input body: real diff when the renderer is available (step: diff
1022
+ // renderer), <pre> fallback otherwise.
1023
+ function _toolCardInputHtml(meta, m) {
1024
+ if (meta.kind === 'patch' && typeof MR.renderPatchDiffHtml === 'function') {
1025
+ const diff = MR.renderPatchDiffHtml(meta, m);
1026
+ if (diff) return diff;
1027
+ }
1028
+ const input = _toolCardInputText(meta, m);
1029
+ if (!input) return '';
1030
+ return '<div class="tool-card-section tool-card-input">'
1031
+ + '<div class="tool-card-section-label">Input</div>'
1032
+ + '<pre><code>' + escHtml(input) + '</code></pre>'
1033
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
1034
+ + '</div>';
1035
+ }
1036
+
1037
+ function _toolResultStatus(resultMeta) {
1038
+ if (!resultMeta) return { status: 'pending', label: '' };
1039
+ const exitCode = Number(resultMeta.exitCode);
1040
+ const isError = resultMeta.isError === true || (Number.isFinite(exitCode) && exitCode !== 0);
1041
+ const parts = [];
1042
+ if (Number.isFinite(exitCode) && exitCode !== 0) parts.push('exit ' + exitCode);
1043
+ else if (isError) parts.push('error');
1044
+ const durationMs = Number(resultMeta.durationMs);
1045
+ if (Number.isFinite(durationMs) && durationMs > 0) {
1046
+ parts.push(durationMs >= 1000 ? (durationMs / 1000).toFixed(1) + 's' : durationMs + 'ms');
1047
+ }
1048
+ return { status: isError ? 'error' : 'ok', label: parts.join(' · ') };
1049
+ }
1050
+
1051
+ function _toolResultOutputHtml(resultMsg) {
1052
+ if (!resultMsg) return '';
1053
+ const meta = MR.messageMeta(resultMsg) || {};
1054
+ const output = _textAfterMarkerLine(resultMsg.text);
1055
+ if (typeof MR.renderStructuredPatchHtml === 'function' && Array.isArray(meta.structuredPatch) && meta.structuredPatch.length) {
1056
+ const diff = MR.renderStructuredPatchHtml(meta);
1057
+ if (diff) {
1058
+ return '<div class="tool-card-section tool-card-output">'
1059
+ + '<div class="tool-card-section-label">Result</div>' + diff
1060
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
1061
+ + '</div>';
1062
+ }
1063
+ }
1064
+ if (!output) return '';
1065
+ return '<div class="tool-card-section tool-card-output">'
1066
+ + '<div class="tool-card-section-label">Output</div>'
1067
+ + '<pre><code>' + escHtml(output) + '</code></pre>'
1068
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
1069
+ + '</div>';
1070
+ }
1071
+
1072
+ // call / result are messages ({text, metadata}); either may be null
1073
+ // (call-only = in-flight or interrupted; result-only = orphaned replay).
1074
+ MR.renderToolCardHtml = function (call, result, i) {
1075
+ const callMeta = call ? (MR.messageMeta(call) || {}) : null;
1076
+ const resultMeta = result ? (MR.messageMeta(result) || {}) : null;
1077
+ const meta = callMeta || { kind: 'tool_call', tool: resultMeta && resultMeta.tool };
1078
+ const anchor = call || result || {};
1079
+ const time = anchor.timestamp ? new Date(anchor.timestamp).toLocaleString() : '';
1080
+ const callId = (callMeta && callMeta.callId) || (resultMeta && resultMeta.callId) || '';
1081
+ const { status, label } = _toolResultStatus(resultMeta);
1082
+ const title = _toolCardTitle(meta);
1083
+ const preview = callMeta ? _toolCardPreview(callMeta) : _previewFromText(_textAfterMarkerLine(result && result.text), 90);
1084
+ return '<div class="review-msg tool-card tool-kind-' + escHtml(meta.kind) + '"'
1085
+ + (callId ? ' data-call-id="' + escHtml(callId) + '"' : '')
1086
+ + ' data-status="' + status + '"'
1087
+ + MR.rawJsonAttrHtml(anchor, callMeta || resultMeta || {})
1088
+ + ' onclick="if(!MR.hasTextSelectionInside||!MR.hasTextSelectionInside(this)){this.classList.toggle(\'expanded\')}"'
1089
+ + ' onkeydown="' + _toggleOnKeydownHandler() + '" role="button" tabindex="0" data-msg-idx="' + escHtml(String(i)) + '">'
1090
+ + '<div class="msg-header">'
1091
+ + '<span class="msg-role"><span class="thought-chevron">▶</span><span class="tool-card-name">' + escHtml(title) + '</span></span>'
1092
+ + '<span class="thought-preview">' + escHtml(preview) + '</span>'
1093
+ + '<span class="tool-card-status">' + escHtml(label) + '</span>'
1094
+ + MR.rawJsonButtonHtml()
1095
+ + '<span class="msg-time">' + escHtml(time) + '</span>'
1096
+ + '</div>'
1097
+ + '<div class="msg-text tool-card-body" onclick="event.stopPropagation()">'
1098
+ + (callMeta ? _toolCardInputHtml(callMeta, call) : '')
1099
+ + _toolResultOutputHtml(result)
1100
+ + '</div>'
1101
+ + '</div>';
1102
+ };
1103
+
1104
+ // ------------------------------------------------------------------
1105
+ // Diff rendering: apply_patch text (*** Begin Patch grammar) and Claude
1106
+ // structuredPatch hunks → per-file collapsible diff tables. Reuses the
1107
+ // code-review diff CSS (.cr-diff-table / .cr-diff-line add|del — linked on
1108
+ // both pages via reviews.css); the JS is intentionally NOT shared with
1109
+ // reviews.js, which is entangled with CR state (comments, context expand).
1110
+ // ------------------------------------------------------------------
1111
+
1112
+ // "*** Begin Patch" grammar → [{path, op, movedTo, hunks:[{header, lines:[{type,text}]}]}]
1113
+ MR.parseApplyPatchText = function (text) {
1114
+ const raw = String(text || '');
1115
+ const start = raw.indexOf('*** Begin Patch');
1116
+ if (start === -1) return [];
1117
+ const lines = raw.slice(start).split('\n');
1118
+ const files = [];
1119
+ let file = null;
1120
+ let hunk = null;
1121
+ const openHunk = (header) => {
1122
+ if (!file) return;
1123
+ hunk = { header: header || '', lines: [] };
1124
+ file.hunks.push(hunk);
1125
+ };
1126
+ for (const line of lines) {
1127
+ const fileStart = line.match(/^\*\*\* (Update|Add|Delete) File: (.+)$/);
1128
+ if (fileStart) {
1129
+ file = {
1130
+ path: fileStart[2].trim(),
1131
+ op: fileStart[1].toLowerCase(),
1132
+ movedTo: '',
1133
+ hunks: [],
1134
+ };
1135
+ files.push(file);
1136
+ hunk = null;
1137
+ continue;
1138
+ }
1139
+ if (!file) continue;
1140
+ const moveTo = line.match(/^\*\*\* Move to: (.+)$/);
1141
+ if (moveTo) { file.movedTo = moveTo[1].trim(); continue; }
1142
+ if (/^\*\*\* End Patch/.test(line) || /^\*\*\* Begin Patch/.test(line)) { file = null; hunk = null; continue; }
1143
+ if (/^@@/.test(line)) { openHunk(line.replace(/^@@\s?/, '').trim()); continue; }
1144
+ const first = line[0];
1145
+ if (first === '+' || first === '-' || first === ' ') {
1146
+ if (!hunk) openHunk('');
1147
+ hunk.lines.push({
1148
+ type: first === '+' ? 'add' : (first === '-' ? 'del' : 'ctx'),
1149
+ text: line.slice(1),
1150
+ });
1151
+ }
1152
+ }
1153
+ return files.filter(f => f.hunks.length || f.op === 'delete');
1154
+ };
1155
+
1156
+ // Claude toolUseResult.structuredPatch (jsdiff hunks) → the same file shape.
1157
+ MR.normalizeStructuredPatch = function (meta) {
1158
+ const hunksIn = meta && Array.isArray(meta.structuredPatch) ? meta.structuredPatch : [];
1159
+ if (!hunksIn.length) return [];
1160
+ const hunks = hunksIn.map(h => ({
1161
+ header: `-${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines}`,
1162
+ oldStart: Number(h.oldStart) || 0,
1163
+ newStart: Number(h.newStart) || 0,
1164
+ lines: (Array.isArray(h.lines) ? h.lines : []).map(l => {
1165
+ const s = String(l);
1166
+ const first = s[0];
1167
+ return {
1168
+ type: first === '+' ? 'add' : (first === '-' ? 'del' : 'ctx'),
1169
+ text: first === '+' || first === '-' || first === ' ' ? s.slice(1) : s,
1170
+ };
1171
+ }),
1172
+ }));
1173
+ return [{ path: String(meta.filePath || ''), op: 'update', movedTo: '', hunks }];
1174
+ };
1175
+
1176
+ MR.renderDiffHtml = function (files) {
1177
+ const list = Array.isArray(files) ? files : [];
1178
+ if (!list.length) return '';
1179
+ return list.map(file => {
1180
+ let adds = 0;
1181
+ let dels = 0;
1182
+ const rows = [];
1183
+ for (const hunk of file.hunks || []) {
1184
+ if (hunk.header) {
1185
+ rows.push('<tr class="cr-diff-hunk-header"><td colspan="4">@@ ' + escHtml(hunk.header) + ' @@</td></tr>');
1186
+ }
1187
+ let oldNum = Number(hunk.oldStart) || 0;
1188
+ let newNum = Number(hunk.newStart) || 0;
1189
+ const hasNums = oldNum > 0 || newNum > 0;
1190
+ for (const line of hunk.lines || []) {
1191
+ const cls = line.type === 'add' ? ' add' : (line.type === 'del' ? ' del' : '');
1192
+ if (line.type === 'add') adds++;
1193
+ if (line.type === 'del') dels++;
1194
+ let oldCell = '';
1195
+ let newCell = '';
1196
+ if (hasNums) {
1197
+ if (line.type !== 'add') oldCell = String(oldNum++);
1198
+ if (line.type !== 'del') newCell = String(newNum++);
1199
+ }
1200
+ rows.push('<tr class="cr-diff-line' + cls + '">'
1201
+ + '<td class="cr-line-num">' + oldCell + '</td>'
1202
+ + '<td class="cr-line-num">' + newCell + '</td>'
1203
+ + '<td class="cr-line-prefix">' + (line.type === 'add' ? '+' : (line.type === 'del' ? '−' : '')) + '</td>'
1204
+ + '<td class="cr-line-content">' + escHtml(line.text) + '</td>'
1205
+ + '</tr>');
1206
+ }
1207
+ }
1208
+ const stat = (adds ? '+' + adds : '') + (adds && dels ? ' ' : '') + (dels ? '−' + dels : '');
1209
+ const pathLabel = file.path + (file.movedTo ? ' → ' + file.movedTo : '');
1210
+ return '<div class="mr-diff-file">'
1211
+ + '<div class="mr-diff-file-header" onclick="this.parentElement.classList.toggle(\'collapsed\');event.stopPropagation()">'
1212
+ + '<span class="mr-diff-op ' + escHtml(file.op || 'update') + '">' + escHtml(file.op || 'update') + '</span>'
1213
+ + '<span class="mr-diff-path">' + escHtml(pathLabel) + '</span>'
1214
+ + (stat ? '<span class="mr-diff-stat">' + escHtml(stat) + '</span>' : '')
1215
+ + '</div>'
1216
+ + (rows.length ? '<table class="cr-diff-table">' + rows.join('') + '</table>' : '')
1217
+ + '</div>';
1218
+ }).join('');
1219
+ };
1220
+
1221
+ // Tool-card hooks (declared in the tool-card section): patch-kind input and
1222
+ // structuredPatch results render as real diffs.
1223
+ MR.renderPatchDiffHtml = function (meta, m) {
1224
+ const files = MR.parseApplyPatchText(_textAfterMarkerLine(m && m.text));
1225
+ if (!files.length) return '';
1226
+ return '<div class="tool-card-section tool-card-input">'
1227
+ + '<div class="tool-card-section-label">Patch</div>'
1228
+ + MR.renderDiffHtml(files)
1229
+ + (meta && meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
1230
+ + '</div>';
1231
+ };
1232
+
1233
+ MR.renderStructuredPatchHtml = function (meta) {
1234
+ return MR.renderDiffHtml(MR.normalizeStructuredPatch(meta));
1235
+ };
1236
+
1237
+ // Live fill-in: a tool_result event lands for a card already in the DOM
1238
+ // (matched on data-call-id by the streaming append path).
1239
+ MR.fillToolCardResult = function (cardEl, resultMsg) {
1240
+ if (!cardEl || !resultMsg) return false;
1241
+ const resultMeta = MR.messageMeta(resultMsg) || {};
1242
+ const { status, label } = _toolResultStatus(resultMeta);
1243
+ cardEl.setAttribute('data-status', status);
1244
+ const statusEl = cardEl.querySelector('.tool-card-status');
1245
+ if (statusEl) statusEl.textContent = label;
1246
+ const body = cardEl.querySelector('.tool-card-body');
1247
+ if (body) {
1248
+ const existing = body.querySelector('.tool-card-output');
1249
+ if (existing) existing.remove();
1250
+ const outputHtml = _toolResultOutputHtml(resultMsg);
1251
+ if (outputHtml) {
1252
+ const el = MR._cardElFromHtml(outputHtml);
1253
+ if (el) body.appendChild(el);
1254
+ }
1255
+ }
1256
+ return true;
1257
+ };
1258
+
1259
+ MR._cardElFromHtml = function (html) {
1260
+ const trimmed = String(html || '').trim();
1261
+ if (!trimmed) return null;
1262
+ const tpl = document.createElement('template');
1263
+ // Trusted structural HTML from our own card renderers; message bodies
1264
+ // inside it have already been escaped/sanitized (escHtml/formatMsgText).
1265
+ tpl.innerHTML = trimmed;
1266
+ return tpl.content.firstElementChild;
1267
+ };
1268
+
1269
+ // Hover-revealed {} button + lazy <pre> with the message's JSON. The JSON
1270
+ // rides in data-raw-json (escaped attribute); textContent assignment means
1271
+ // no sanitization concerns on display.
1272
+ MR.rawJsonAttrHtml = function (m, meta) {
1273
+ const payload = { role: m.role, text: m.text, timestamp: m.timestamp, metadata: meta };
1274
+ try { return ' data-raw-json="' + escHtml(JSON.stringify(payload)) + '"'; } catch { return ''; }
1275
+ };
1276
+
1277
+ MR.rawJsonButtonHtml = function () {
1278
+ return '<button class="msg-raw-btn" title="Show raw JSON" '
1279
+ + 'onclick="MR.toggleRawJson(this);event.stopPropagation()">{}</button>';
1280
+ };
1281
+
1282
+ MR.toggleRawJson = function (btn) {
1283
+ const row = btn && btn.closest ? btn.closest('[data-raw-json]') : null;
1284
+ if (!row) return;
1285
+ let pre = row.querySelector('.msg-raw-json');
1286
+ if (!pre) {
1287
+ pre = document.createElement('pre');
1288
+ pre.className = 'msg-raw-json';
1289
+ let raw = row.getAttribute('data-raw-json') || '';
1290
+ try { raw = JSON.stringify(JSON.parse(raw), null, 2); } catch { /* show as-is */ }
1291
+ pre.textContent = raw;
1292
+ row.appendChild(pre);
1293
+ return;
1294
+ }
1295
+ pre.classList.toggle('hidden');
1296
+ };
1297
+
1298
+ MR.renderUnknownKindHtml = function (m, i, meta) {
1299
+ const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
1300
+ const kindLabel = String(meta.kindHint || meta.kind || 'unknown');
1301
+ const raw = typeof meta.raw === 'string' && meta.raw ? meta.raw : JSON.stringify(meta);
1302
+ return '<div class="review-msg local-cmd unknown-kind"' + MR.rawJsonAttrHtml(m, meta)
1303
+ + ' onclick="this.classList.toggle(\'expanded\')" onkeydown="' + _toggleOnKeydownHandler() + '" role="button" tabindex="0" data-msg-idx="' + escHtml(String(i)) + '">'
1304
+ + '<div class="msg-header">'
1305
+ + '<span class="msg-role"><span class="thought-chevron">▶</span>Unrecorded event</span>'
1306
+ + '<span class="thought-preview">' + escHtml('kind: ' + kindLabel) + '</span>'
1307
+ + MR.rawJsonButtonHtml()
1308
+ + '<span class="msg-time">' + escHtml(time) + '</span>'
1309
+ + '</div>'
1310
+ + '<div class="msg-text"><pre><code>' + escHtml(raw) + '</code></pre>'
1311
+ + (meta.truncated ? '<div class="msg-truncation-note">(truncated)</div>' : '')
1312
+ + '</div>'
1313
+ + '</div>';
1314
+ };
1315
+
612
1316
  // ------------------------------------------------------------------
613
1317
  // Message classification (was index.html classifyMessage).
614
1318
  // ------------------------------------------------------------------
1319
+ function _isSubagentPromptMessage(m) {
1320
+ const metadata = m && m.metadata && typeof m.metadata === 'object' ? m.metadata : {};
1321
+ return m && m.role === 'system' && metadata.sourceKind === 'subagent' && metadata.originalRole === 'user';
1322
+ }
1323
+
615
1324
  MR.classifyMessage = function (m, stripped, isToolOnly) {
616
1325
  const text = m.text || '';
1326
+ // Structured-capture rows: meta-first, before all text heuristics. Kinds
1327
+ // whose card renderer hasn't landed yet fall through to the legacy
1328
+ // pipeline (their text fallback renders exactly like pre-capture rows).
1329
+ const meta = MR.messageMeta(m);
1330
+ if (meta) {
1331
+ if (meta.kind === 'unknown' || !_KNOWN_META_KINDS.has(meta.kind)) return 'unknown-kind';
1332
+ if (_TOOL_CARD_KINDS.has(meta.kind)) return 'tool-card';
1333
+ if (meta.kind === 'tool_result') return 'tool-card-result';
1334
+ if (meta.kind === 'reasoning') return 'thinking';
1335
+ if (meta.kind === 'compact_boundary') return 'compact-boundary';
1336
+ if (meta.kind === 'compact_summary') return 'compact-summary';
1337
+ }
617
1338
  if ((m.role === 'assistant' || m.role === 'system') && MR.isCodexOperationalWarning(text)) return 'codex-warning';
1339
+ if (m.role === 'system' && /^\[Wall-E (?:error|aborted)\]/i.test(text)) return 'walle-error';
1340
+ if (_isSubagentPromptMessage(m)) return 'subagent-prompt';
618
1341
  // System role messages (tool results, task notifications) — treat as self-thought
619
1342
  if (m.role === 'system') return 'self-thought';
620
1343
  // User-role messages that are actually slash commands or injected skill
@@ -691,8 +1414,10 @@
691
1414
  }
692
1415
 
693
1416
  function _isPromptItem(item) {
694
- if (!item || !item.m || item.m.role !== 'user') return false;
1417
+ if (!item || !item.m) return false;
695
1418
  const text = String(item.m.text || '').trim();
1419
+ if (item.msgType === 'subagent-prompt') return !!text;
1420
+ if (item.m.role !== 'user') return false;
696
1421
  if (!text && item.msgType !== 'command') return false;
697
1422
  return item.msgType === 'key' || item.msgType === 'command';
698
1423
  }
@@ -712,8 +1437,11 @@
712
1437
  let detail = 0;
713
1438
  for (const item of responses) {
714
1439
  if (item.m.role === 'assistant') assistant++;
715
- tools += _toolNamesFromText(item.m.text).length;
716
- if (item.msgType === 'self-thought' || item.msgType === 'skill-body' || item.msgType === 'local-cmd' || item.msgType === 'tool-result' || item.msgType === 'summary') detail++;
1440
+ // Tool cards count once each (their text fallback may or may not carry
1441
+ // a [Tool: …] token, so counting the card avoids double/zero counts).
1442
+ if (item.msgType === 'tool-card') tools += 1;
1443
+ else tools += _toolNamesFromText(item.m.text).length;
1444
+ if (item.msgType === 'self-thought' || item.msgType === 'skill-body' || item.msgType === 'local-cmd' || item.msgType === 'tool-result' || item.msgType === 'tool-card-result' || item.msgType === 'summary') detail++;
717
1445
  }
718
1446
  return { assistant, tools, detail, total: responses.length };
719
1447
  }
@@ -739,6 +1467,19 @@
739
1467
  if (counts.tools) badges.push(counts.tools + ' tool' + (counts.tools === 1 ? '' : 's'));
740
1468
  if (counts.detail) badges.push(counts.detail + ' detail' + (counts.detail === 1 ? '' : 's'));
741
1469
  if (!counts.total && turn.type !== 'setup') badges.push('no response yet');
1470
+ // Token chip: sum assistant usage metadata across the turn's responses.
1471
+ let usageTotal = 0;
1472
+ const usageSum = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
1473
+ for (const item of turn.responses || []) {
1474
+ const usage = _usageFromMessage(item.m);
1475
+ if (!usage) continue;
1476
+ usageTotal += MR.usageTokensTotal(usage);
1477
+ for (const key of Object.keys(usageSum)) usageSum[key] += Number(usage[key]) || 0;
1478
+ }
1479
+ if (usageTotal > 0) {
1480
+ badges.push('<span class="prompt-turn-badge token-chip" title="' + escHtml(MR.usageTitle(usageSum)) + '">'
1481
+ + escHtml(_fmtTokens(usageTotal) + ' tok') + '</span>');
1482
+ }
742
1483
  const alert = _turnAlert(turn);
743
1484
  if (alert) badges.push('<span class="prompt-turn-badge prompt-turn-alert ' + alert.level + '">' + escHtml(alert.label) + '</span>');
744
1485
  return badges.map(b => {
@@ -751,18 +1492,24 @@
751
1492
  const m = item.m || {};
752
1493
  const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
753
1494
  const cmd = item.msgType === 'command' ? MR.parseCommandInvocation(m.text) : null;
1495
+ const isSubagentPrompt = item.msgType === 'subagent-prompt';
1496
+ const displayText = cmd ? (cmd.args || '') : (m.text || '');
1497
+ const attachmentHtml = _appendReviewAttachmentsHtml(Object.assign({}, m, { text: displayText }));
754
1498
  const bodyHtml = cmd
755
- ? (cmd.args ? MR.formatMsgText(cmd.args) : '<span class="msg-empty">(no arguments)</span>')
756
- : MR.formatMsgText(m.text || '');
1499
+ ? _formatDisplayTextHtml(displayText, attachmentHtml ? '<span class="msg-empty">(image attachment)</span>' : '<span class="msg-empty">(no arguments)</span>')
1500
+ : _formatDisplayTextHtml(displayText, attachmentHtml ? '<span class="msg-empty">(image attachment)</span>' : '');
757
1501
  const badge = cmd ? '<span class="msg-cmd-badge" title="Slash command">/' + escHtml(cmd.name) + '</span>' : '';
758
1502
  const parentUuid = m.parentUuid ? ' data-parent-uuid="' + escHtml(m.parentUuid) + '"' : '';
759
- return '<div class="review-msg user key-msg prompt-turn-prompt"' + parentUuid + '>'
1503
+ const roleClass = isSubagentPrompt ? 'subagent-prompt' : 'user';
1504
+ const roleLabel = isSubagentPrompt ? (m.agentLabel || m.roleLabel || 'Subagent prompt') : 'You';
1505
+ return '<div class="review-msg ' + roleClass + ' key-msg prompt-turn-prompt"' + parentUuid + '>'
760
1506
  + '<div class="msg-header">'
761
- + '<span class="msg-role">You</span>'
1507
+ + '<span class="msg-role">' + escHtml(roleLabel) + '</span>'
762
1508
  + badge
763
1509
  + '<span class="msg-time">' + escHtml(time) + '</span>'
764
1510
  + '</div>'
765
1511
  + '<div class="msg-text" data-msg-idx="' + item.i + '">' + bodyHtml + '</div>'
1512
+ + attachmentHtml
766
1513
  + '</div>';
767
1514
  }
768
1515
 
@@ -839,12 +1586,16 @@
839
1586
  // ------------------------------------------------------------------
840
1587
 
841
1588
  MR.renderReviewMsg = function (m, i, msgType) {
1589
+ const metaCard = MR.renderMetaCardHtml(m, i);
1590
+ if (metaCard) return metaCard;
842
1591
  const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
843
- const textHtml = MR.formatMsgText(m.text);
1592
+ const displayText = _messageTextForDisplay(m.text || '');
1593
+ const attachmentHtml = _appendReviewAttachmentsHtml(Object.assign({}, m, { text: m.text || '' }));
1594
+ const textHtml = _formatDisplayTextHtml(m.text || '', attachmentHtml ? '<span class="msg-empty">(image attachment)</span>' : '');
844
1595
  const hasRenderedText = !!String(textHtml || '').trim();
845
- const needsCollapse = m.text.length > 1500 || m.text.split('\n').length > 30;
846
- const stripped = m.text.replace(/\[Tool: [^\]]+\]/g, '').trim();
847
- const isToolOnly = stripped.length === 0 && /\[Tool: /.test(m.text);
1596
+ const needsCollapse = displayText.length > 1500 || displayText.split('\n').length > 30;
1597
+ const stripped = displayText.replace(/\[Tool: [^\]]+\]/g, '').trim();
1598
+ const isToolOnly = stripped.length === 0 && /\[Tool: /.test(displayText);
848
1599
  if (!msgType) msgType = MR.classifyMessage(m, stripped, isToolOnly);
849
1600
 
850
1601
  if (msgType === 'command') {
@@ -852,7 +1603,9 @@
852
1603
  // after `/name`. Render as a real user message but with a small badge
853
1604
  // identifying the command — the badge replaces the raw envelope clutter.
854
1605
  const cmd = MR.parseCommandInvocation(m.text) || { name: '', args: '' };
855
- const argsHtml = cmd.args ? MR.formatMsgText(cmd.args) : '<span class="msg-empty">(no arguments)</span>';
1606
+ const cmdMessage = Object.assign({}, m, { text: cmd.args || '' });
1607
+ const cmdAttachmentHtml = _appendReviewAttachmentsHtml(cmdMessage);
1608
+ const argsHtml = _formatDisplayTextHtml(cmd.args || '', cmdAttachmentHtml ? '<span class="msg-empty">(image attachment)</span>' : '<span class="msg-empty">(no arguments)</span>');
856
1609
  return '<div class="review-msg user key-msg command-invocation">'
857
1610
  + '<div class="msg-header">'
858
1611
  + '<span class="msg-role">You</span>'
@@ -860,6 +1613,7 @@
860
1613
  + '<span class="msg-time">' + escHtml(time) + '</span>'
861
1614
  + '</div>'
862
1615
  + '<div class="msg-text" data-msg-idx="' + i + '">' + argsHtml + '</div>'
1616
+ + cmdAttachmentHtml
863
1617
  + '</div>';
864
1618
  }
865
1619
 
@@ -916,15 +1670,17 @@
916
1670
  + '</div>';
917
1671
  }
918
1672
 
919
- const isKey = msgType === 'key';
920
- if (!hasRenderedText && !isToolOnly) return '';
921
- const roleLabel = m.role === 'user' ? 'You' : (m.agentLabel || m.roleLabel || 'Claude');
922
- return '<div class="review-msg ' + m.role + (isToolOnly ? ' tool-only' : '') + (isKey ? ' key-msg' : '') + '">'
1673
+ const isWalleError = msgType === 'walle-error';
1674
+ const isKey = msgType === 'key' || isWalleError;
1675
+ if (!hasRenderedText && !isToolOnly && !attachmentHtml) return '';
1676
+ const roleLabel = isWalleError ? 'Wall-E Error' : (m.role === 'user' ? 'You' : (m.agentLabel || m.roleLabel || 'Claude'));
1677
+ return '<div class="review-msg ' + m.role + (isToolOnly ? ' tool-only' : '') + (isKey ? ' key-msg' : '') + (isWalleError ? ' walle-error-msg' : '') + '">'
923
1678
  + '<div class="msg-header">'
924
1679
  + '<span class="msg-role">' + escHtml(roleLabel) + '</span>'
925
1680
  + '<span class="msg-time">' + escHtml(time) + '</span>'
926
1681
  + '</div>'
927
1682
  + '<div class="msg-text' + (needsCollapse ? ' collapsed' : '') + '" data-msg-idx="' + i + '">' + textHtml + '</div>'
1683
+ + attachmentHtml
928
1684
  + (needsCollapse ? '<button class="msg-expand visible" onclick="toggleMsgExpand(this)">Show more</button>' : '')
929
1685
  + '</div>';
930
1686
  };
@@ -964,6 +1720,32 @@
964
1720
 
965
1721
  MR.groupMessages = function (messages) {
966
1722
  const groups = [];
1723
+ // Tool-call ↔ tool-result pairing by metadata.callId: a result claims
1724
+ // the FIRST unclaimed call with its id; claimed results are skipped at
1725
+ // their own index (they render inside the call's card). Results can be
1726
+ // non-adjacent (parallel tool calls interleave).
1727
+ const resultIdxByCallId = new Map();
1728
+ const claimedResultIdxs = new Set();
1729
+ for (let ri = 0; ri < messages.length; ri += 1) {
1730
+ const rMeta = MR.messageMeta(messages[ri]);
1731
+ if (rMeta && rMeta.kind === 'tool_result' && rMeta.callId && !resultIdxByCallId.has(rMeta.callId)) {
1732
+ resultIdxByCallId.set(rMeta.callId, ri);
1733
+ }
1734
+ }
1735
+ let sessionWarningItems = null;
1736
+ let sessionWarningsInserted = false;
1737
+ const collectSessionWarnings = () => {
1738
+ if (sessionWarningItems) return sessionWarningItems;
1739
+ sessionWarningItems = [];
1740
+ for (let wi = 0; wi < messages.length; wi += 1) {
1741
+ const wm = messages[wi];
1742
+ const ws = wm.text.replace(/\[Tool: [^\]]+\]/g, '').trim();
1743
+ const wto = ws.length === 0 && /\[Tool: /.test(wm.text);
1744
+ if (MR.classifyMessage(wm, ws, wto) !== 'codex-warning') continue;
1745
+ sessionWarningItems.push({ m: wm, i: typeof wm._reviewIdx === 'number' ? wm._reviewIdx : wi, stripped: ws, isToolOnly: wto });
1746
+ }
1747
+ return sessionWarningItems;
1748
+ };
967
1749
  let i = 0;
968
1750
  while (i < messages.length) {
969
1751
  const m = messages[i];
@@ -972,17 +1754,29 @@
972
1754
  const isToolOnly = stripped.length === 0 && /\[Tool: /.test(m.text);
973
1755
  const msgType = MR.classifyMessage(m, stripped, isToolOnly);
974
1756
 
975
- if (msgType === 'codex-warning') {
976
- const warningItems = [];
977
- while (i < messages.length) {
978
- const gm = messages[i];
979
- const gs = gm.text.replace(/\[Tool: [^\]]+\]/g, '').trim();
980
- const gto = gs.length === 0 && /\[Tool: /.test(gm.text);
981
- if (MR.classifyMessage(gm, gs, gto) !== 'codex-warning') break;
982
- warningItems.push({ m: gm, i: typeof gm._reviewIdx === 'number' ? gm._reviewIdx : i, stripped: gs, isToolOnly: gto });
983
- i++;
1757
+ if (msgType === 'tool-card') {
1758
+ const callMeta = MR.messageMeta(m);
1759
+ let result = null;
1760
+ const resultIdx = callMeta && callMeta.callId ? resultIdxByCallId.get(callMeta.callId) : undefined;
1761
+ if (resultIdx !== undefined && resultIdx > i && !claimedResultIdxs.has(resultIdx)) {
1762
+ claimedResultIdxs.add(resultIdx);
1763
+ const rm = messages[resultIdx];
1764
+ result = { m: rm, i: typeof rm._reviewIdx === 'number' ? rm._reviewIdx : resultIdx };
1765
+ }
1766
+ groups.push({ type: 'tool-card', call: { m, i: idx }, result });
1767
+ i++;
1768
+ } else if (msgType === 'tool-card-result') {
1769
+ if (!claimedResultIdxs.has(i)) {
1770
+ // Orphaned result (call outside this page/window) — completed card.
1771
+ groups.push({ type: 'tool-card', call: null, result: { m, i: idx } });
984
1772
  }
985
- groups.push({ type: 'codex-warning-group', items: warningItems });
1773
+ i++;
1774
+ } else if (msgType === 'codex-warning') {
1775
+ if (!sessionWarningsInserted) {
1776
+ groups.push({ type: 'codex-warning-group', items: collectSessionWarnings() });
1777
+ sessionWarningsInserted = true;
1778
+ }
1779
+ i++;
986
1780
  } else if (msgType === 'self-thought') {
987
1781
  const groupItems = [];
988
1782
  while (i < messages.length) {
@@ -1006,6 +1800,9 @@
1006
1800
  if (g.type === 'message') {
1007
1801
  return MR.renderReviewMsg(g.m, g.i, g.msgType);
1008
1802
  }
1803
+ if (g.type === 'tool-card') {
1804
+ return MR.renderToolCardHtml(g.call && g.call.m, g.result && g.result.m, g.call ? g.call.i : g.result.i);
1805
+ }
1009
1806
  if (g.type === 'codex-warning-group') {
1010
1807
  return MR.renderCodexWarningGroup(g.items || []);
1011
1808
  }
@@ -1185,6 +1982,125 @@
1185
1982
  return group;
1186
1983
  };
1187
1984
 
1985
+ function _createSystemActivityItemRow(evt, time) {
1986
+ const text = _conversationEventTextValue(evt);
1987
+ const row = document.createElement('div');
1988
+ row.className = 'review-msg system system-activity-item';
1989
+ if (evt?.data?.parentUuid) row.dataset.parentUuid = evt.data.parentUuid;
1990
+ if (time) row.dataset.time = time;
1991
+ row.dataset.systemPreview = _summaryPreview(text) || 'No details recorded';
1992
+
1993
+ const header = document.createElement('div');
1994
+ header.className = 'msg-header';
1995
+ const role = document.createElement('span');
1996
+ role.className = 'msg-role';
1997
+ role.textContent = 'System';
1998
+ const preview = document.createElement('span');
1999
+ preview.className = 'thought-preview';
2000
+ preview.textContent = row.dataset.systemPreview;
2001
+ header.appendChild(role);
2002
+ header.appendChild(preview);
2003
+ if (time) {
2004
+ const t = document.createElement('span');
2005
+ t.className = 'msg-time';
2006
+ t.textContent = time;
2007
+ header.appendChild(t);
2008
+ }
2009
+ row.appendChild(header);
2010
+
2011
+ const body = document.createElement('div');
2012
+ body.className = 'msg-text';
2013
+ if (text) {
2014
+ const formatted = MR.formatMsgText(_messageTextForDisplay(text));
2015
+ body.innerHTML = (typeof DOMPurify !== 'undefined') ? DOMPurify.sanitize(formatted) : formatted;
2016
+ } else {
2017
+ const empty = document.createElement('span');
2018
+ empty.className = 'msg-empty';
2019
+ empty.textContent = '(no details recorded)';
2020
+ body.appendChild(empty);
2021
+ }
2022
+ row.appendChild(body);
2023
+ return row;
2024
+ }
2025
+
2026
+ function _isLowValueSystemSummary(evt, text, toolUses) {
2027
+ if (String(text || '').trim()) return false;
2028
+ if (Array.isArray(toolUses) && toolUses.length) return false;
2029
+ const data = evt && evt.data && typeof evt.data === 'object' ? evt.data : {};
2030
+ const content = data.content ?? evt?.content;
2031
+ if (content == null) return true;
2032
+ return !_conversationContentToText(content).trim();
2033
+ }
2034
+
2035
+ MR.refreshConversationSystemGroup = function (group) {
2036
+ if (!group || !group.querySelector) return;
2037
+ const items = group.querySelector('.thought-group-items');
2038
+ if (!items) return;
2039
+ const rows = Array.from(items.children || []);
2040
+ const countEl = group.querySelector('.thought-group-count');
2041
+ if (countEl) {
2042
+ const eventCount = rows.length;
2043
+ countEl.textContent = '— ' + eventCount + ' event' + (eventCount === 1 ? '' : 's');
2044
+ }
2045
+ const previews = [];
2046
+ for (const row of rows) {
2047
+ const p = row.dataset?.systemPreview || '';
2048
+ if (p && !previews.includes(p)) previews.push(p);
2049
+ }
2050
+ const previewEl = group.querySelector('.thought-group-preview');
2051
+ if (previewEl) {
2052
+ previewEl.textContent = previews.length === 1 ? previews[0] : (previews[0] || 'No details recorded');
2053
+ }
2054
+ const timeEl = group.querySelector('.thought-group-time');
2055
+ if (timeEl) {
2056
+ const f = group.dataset.firstTime || rows[0]?.dataset?.time || '';
2057
+ const l = group.dataset.lastTime || rows[rows.length - 1]?.dataset?.time || '';
2058
+ timeEl.textContent = (f && l && f !== l) ? f + ' – ' + l : (l || f);
2059
+ }
2060
+ };
2061
+
2062
+ MR.createConversationSystemGroup = function (evt, time) {
2063
+ const group = document.createElement('div');
2064
+ group.className = 'thought-group conv-system-group system-activity-group';
2065
+ group.dataset.firstTime = time || '';
2066
+ group.dataset.lastTime = time || '';
2067
+ group.addEventListener('click', () => group.classList.toggle('expanded'));
2068
+
2069
+ const header = document.createElement('div');
2070
+ header.className = 'thought-group-header';
2071
+ const label = document.createElement('span');
2072
+ label.className = 'thought-group-label';
2073
+ const chev = document.createElement('span');
2074
+ chev.className = 'thought-chevron';
2075
+ chev.textContent = '▶';
2076
+ label.appendChild(chev);
2077
+ const labelText = document.createElement('span');
2078
+ labelText.textContent = 'System';
2079
+ label.appendChild(labelText);
2080
+ const count = document.createElement('span');
2081
+ count.className = 'thought-group-count';
2082
+ count.textContent = '— 1 event';
2083
+ label.appendChild(count);
2084
+
2085
+ const preview = document.createElement('span');
2086
+ preview.className = 'thought-group-preview';
2087
+ preview.textContent = 'No details recorded';
2088
+ const tEl = document.createElement('span');
2089
+ tEl.className = 'thought-group-time';
2090
+ tEl.textContent = time || '';
2091
+ header.appendChild(label);
2092
+ header.appendChild(preview);
2093
+ header.appendChild(tEl);
2094
+
2095
+ const items = document.createElement('div');
2096
+ items.className = 'thought-group-items';
2097
+ items.appendChild(_createSystemActivityItemRow(evt, time));
2098
+ group.appendChild(header);
2099
+ group.appendChild(items);
2100
+ MR.refreshConversationSystemGroup(group);
2101
+ return group;
2102
+ };
2103
+
1188
2104
  // ------------------------------------------------------------------
1189
2105
  // Conversation-tab renderers — return DOM elements with event
1190
2106
  // listeners attached directly. Used inside the active session view
@@ -1236,6 +2152,10 @@
1236
2152
 
1237
2153
  MR.refreshConversationActivityGroup = function (group) {
1238
2154
  if (!group || !group.querySelector) return;
2155
+ if (_isConversationCombinedActivityGroup(group)) {
2156
+ MR.refreshConversationCombinedActivityGroup(group);
2157
+ return;
2158
+ }
1239
2159
  const items = group.querySelector('.thought-group-items');
1240
2160
  if (!items) return;
1241
2161
  const rows = Array.from(items.children || []);
@@ -1281,14 +2201,150 @@
1281
2201
  // 'summary'). In the unit-test harness, the envelope shape is
1282
2202
  // `{ type: 'stream-event', data: { type: 'assistant', ... } }`. Look
1283
2203
  // at evt.type first, fall back to evt.data.type.
1284
- const _ROLES = ['user', 'assistant', 'tool_result', 'summary'];
2204
+ const _ROLES = ['user', 'assistant', 'tool_result', 'summary', 'system'];
2205
+
2206
+ function _conversationContentToText(content) {
2207
+ if (content == null) return '';
2208
+ if (typeof content === 'string') return content;
2209
+ if (Array.isArray(content)) {
2210
+ return content.map((part) => {
2211
+ if (!part) return '';
2212
+ if (typeof part === 'string') return part;
2213
+ if (part.type === 'text' || part.type === 'input_text') return part.text || part.content || '';
2214
+ if (part.type === 'tool_result') return part.content || part.text || '';
2215
+ if (part.type === 'tool_use') return '[Tool: ' + (part.name || 'tool') + ']';
2216
+ if (part.text || part.content || part.message) return part.text || part.content || part.message;
2217
+ return '';
2218
+ }).filter(Boolean).join('\n');
2219
+ }
2220
+ if (content && typeof content === 'object') {
2221
+ if (content.text || content.content || content.message) return _conversationContentToText(content.text || content.content || content.message);
2222
+ try { return JSON.stringify(content); } catch { return String(content); }
2223
+ }
2224
+ return String(content || '');
2225
+ }
2226
+
2227
+ function _conversationEventTextValue(evt) {
2228
+ const data = evt && evt.data && typeof evt.data === 'object' ? evt.data : {};
2229
+ return _conversationContentToText(
2230
+ data.text ?? data.message ?? data.content ?? evt?.text ?? evt?.message ?? evt?.content ?? ''
2231
+ ).trim();
2232
+ }
2233
+
2234
+ function _stripSystemReminderText(text) {
2235
+ return String(text || '')
2236
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, '')
2237
+ .replace(/<\/?system-reminder[^>]*>/gi, '')
2238
+ .trim();
2239
+ }
2240
+
2241
+ function _isSystemOnlyUserEvent(evt, text) {
2242
+ const data = evt && evt.data && typeof evt.data === 'object' ? evt.data : {};
2243
+ if (data.system === true || data.isSystem === true || data.role === 'system') return true;
2244
+ const content = data.content ?? evt?.content;
2245
+ if (Array.isArray(content) && content.some(part => part && (part.type === 'tool_result' || part.type === 'tool_use'))) return true;
2246
+ const raw = String(text || '');
2247
+ if (!raw) return true;
2248
+ if (raw.includes('<system-reminder>') && !_stripSystemReminderText(raw)) return true;
2249
+ if (raw.includes('<task-notification>')) return true;
2250
+ if (raw.includes('toolu_') && raw.includes('.output')) return true;
2251
+ return false;
2252
+ }
2253
+
2254
+ function _summaryPreview(text) {
2255
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
2256
+ return value.length > 110 ? value.slice(0, 110) + '…' : value;
2257
+ }
2258
+
2259
+ function _wireSummaryToggle(row, header) {
2260
+ if (!row || !header) return;
2261
+ const setExpanded = (expanded) => {
2262
+ row.classList.toggle('expanded', !!expanded);
2263
+ header.setAttribute('aria-expanded', expanded ? 'true' : 'false');
2264
+ };
2265
+ header.setAttribute('role', 'button');
2266
+ header.setAttribute('tabindex', '0');
2267
+ header.setAttribute('aria-expanded', row.classList.contains('expanded') ? 'true' : 'false');
2268
+ const toggle = () => setExpanded(!row.classList.contains('expanded'));
2269
+ header.addEventListener('click', toggle);
2270
+ header.addEventListener('keydown', (ev) => {
2271
+ if (ev.key === 'Enter' || ev.key === ' ') {
2272
+ ev.preventDefault();
2273
+ toggle();
2274
+ }
2275
+ });
2276
+ }
2277
+
2278
+ function _buildConversationSummaryRow(evt, time, opts) {
2279
+ opts = opts || {};
2280
+ const text = _conversationEventTextValue(evt);
2281
+ const row = document.createElement('div');
2282
+ row.className = 'review-msg ' + (opts.roleClass || 'system') + ' summary';
2283
+ if (evt?.data?.parentUuid) row.dataset.parentUuid = evt.data.parentUuid;
2284
+
2285
+ const header = document.createElement('div');
2286
+ header.className = 'msg-header';
2287
+ const role = document.createElement('span');
2288
+ role.className = 'msg-role';
2289
+ const chev = document.createElement('span');
2290
+ chev.className = 'thought-chevron';
2291
+ chev.textContent = '▶';
2292
+ role.appendChild(chev);
2293
+ const label = document.createElement('span');
2294
+ label.textContent = opts.label || 'System';
2295
+ role.appendChild(label);
2296
+ const previewText = _summaryPreview(text) || '(no details recorded)';
2297
+ const preview = document.createElement('span');
2298
+ preview.className = 'thought-preview';
2299
+ preview.textContent = previewText;
2300
+ role.appendChild(preview);
2301
+ header.appendChild(role);
2302
+
2303
+ if (time) {
2304
+ const t = document.createElement('span');
2305
+ t.className = 'msg-time';
2306
+ t.textContent = time;
2307
+ header.appendChild(t);
2308
+ }
2309
+ row.appendChild(header);
2310
+
2311
+ const body = document.createElement('div');
2312
+ body.className = 'msg-text';
2313
+ if (text) {
2314
+ body.innerHTML = MR.formatMsgText(_messageTextForDisplay(text));
2315
+ } else {
2316
+ const empty = document.createElement('span');
2317
+ empty.className = 'msg-empty';
2318
+ empty.textContent = '(no details recorded)';
2319
+ body.appendChild(empty);
2320
+ }
2321
+ row.appendChild(body);
2322
+ _wireSummaryToggle(row, header);
2323
+ return row;
2324
+ }
2325
+
1285
2326
  MR.renderConversationEvent = function (evt) {
1286
- const text = (evt.data?.text || '').trim();
2327
+ // Structured-capture rows render the SAME HTML string the Review page
2328
+ // gets (renderMetaCardHtml), hydrated into a DOM element — parity by
2329
+ // construction. Falls through when the kind has no card renderer.
2330
+ const evtMeta = MR.messageMeta(evt);
2331
+ if (evtMeta) {
2332
+ const metaMsg = {
2333
+ role: (evt.data && evt.data.type) || evt.type || 'system',
2334
+ text: _conversationEventTextValue(evt),
2335
+ timestamp: evt.timestamp || (evt.data && evt.data.timestamp) || '',
2336
+ metadata: evtMeta,
2337
+ };
2338
+ const card = MR._cardElFromHtml(MR.renderMetaCardHtml(metaMsg, evt.data && evt.data.msgIdx || 0));
2339
+ if (card) return card;
2340
+ }
2341
+ const text = _conversationEventTextValue(evt);
1287
2342
  const toolUses = evt.data?.toolUses || [];
1288
2343
 
1289
2344
  let evType = _ROLES.includes(evt.type) ? evt.type : null;
1290
2345
  if (!evType && _ROLES.includes(evt.data?.type)) evType = evt.data.type;
1291
2346
  if (!evType) return null;
2347
+ if (evType === 'system') evType = 'summary';
1292
2348
 
1293
2349
  // Phase 4 reconciliation: empty user events used to be dropped in
1294
2350
  // Conversation (legacy `if (!text) return null`), but Review classifies
@@ -1301,27 +2357,14 @@
1301
2357
  const time = evt.timestamp ? new Date(evt.timestamp).toLocaleString() : '';
1302
2358
 
1303
2359
  if (evType === 'user') {
1304
- if (!text) {
2360
+ if (_isSystemOnlyUserEvent(evt, text)) {
1305
2361
  // System-only user message (tool_result blocks, hooks, etc.) —
1306
2362
  // render as a compact summary row that the user can expand if
1307
2363
  // they want to inspect. Matches Review's classifier output.
1308
- const div = document.createElement('div');
1309
- div.className = 'review-msg user summary';
1310
- if (evt.data?.parentUuid) div.dataset.parentUuid = evt.data.parentUuid;
1311
- const header = document.createElement('div');
1312
- header.className = 'msg-header';
1313
- const role = document.createElement('span');
1314
- role.className = 'msg-role';
1315
- role.textContent = '(system message)';
1316
- header.appendChild(role);
1317
- if (time) {
1318
- const t = document.createElement('span');
1319
- t.className = 'msg-time';
1320
- t.textContent = time;
1321
- header.appendChild(t);
2364
+ if (_isLowValueSystemSummary(evt, text, toolUses)) {
2365
+ return MR.createConversationSystemGroup(evt, time);
1322
2366
  }
1323
- div.appendChild(header);
1324
- return div;
2367
+ return _buildConversationSummaryRow(evt, time, { roleClass: 'user', label: 'System' });
1325
2368
  }
1326
2369
  // Slash-command invocation — extract args; render with a /cmd badge.
1327
2370
  const cmd = MR.parseCommandInvocation(text);
@@ -1337,8 +2380,9 @@
1337
2380
  // Insert the badge between role and time so layout matches Review.
1338
2381
  header.insertBefore(badge, header.querySelector('.msg-time') || null);
1339
2382
  div.appendChild(header);
1340
- if (cmd.args) {
1341
- _appendTextWithExpand(div, cmd.args);
2383
+ const cmdMessage = Object.assign({}, evt.data || {}, { text: cmd.args || '' });
2384
+ if (cmd.args || MR.extractImageAttachments(cmdMessage).length) {
2385
+ _appendTextWithExpand(div, cmd.args, cmdMessage, { emptyHtml: '<span class="msg-empty">(image attachment)</span>' });
1342
2386
  } else {
1343
2387
  const empty = document.createElement('div');
1344
2388
  empty.className = 'msg-text';
@@ -1381,7 +2425,7 @@
1381
2425
  }
1382
2426
  div.appendChild(header);
1383
2427
  // Body uses DOMPurify (defense-in-depth around marked output).
1384
- _appendTextWithExpand(div, text);
2428
+ _appendTextWithExpand(div, text, evt.data || evt);
1385
2429
  return div;
1386
2430
  }
1387
2431
  // Local command output — same compact-collapsible treatment.
@@ -1469,7 +2513,7 @@
1469
2513
  div.className = 'review-msg user';
1470
2514
  if (evt.data?.parentUuid) div.dataset.parentUuid = evt.data.parentUuid;
1471
2515
  div.appendChild(_makeHeader('You', time));
1472
- if (!_appendTextWithExpand(div, text)) return null;
2516
+ if (!_appendTextWithExpand(div, text, evt.data || evt)) return null;
1473
2517
  return div;
1474
2518
  }
1475
2519
 
@@ -1596,10 +2640,15 @@
1596
2640
  const div = document.createElement('div');
1597
2641
  div.className = 'review-msg assistant';
1598
2642
  if (evt.data?.parentUuid) div.dataset.parentUuid = evt.data.parentUuid;
2643
+ const rowUsage = evt.data?.metadata?.usage;
2644
+ if (rowUsage && MR.usageTokensTotal(rowUsage) > 0) {
2645
+ div.dataset.usageTokens = String(MR.usageTokensTotal(rowUsage));
2646
+ div.dataset.usageTitle = MR.usageTitle(rowUsage);
2647
+ }
1599
2648
  const roleLabel = 'Assistant' + (evt.data?.model ? ' (' + evt.data.model + ')' : '');
1600
2649
  div.appendChild(_makeHeader(roleLabel, time));
1601
2650
  if (text) {
1602
- const hasTextBody = _appendTextWithExpand(div, text);
2651
+ const hasTextBody = _appendTextWithExpand(div, text, evt.data || evt);
1603
2652
  if (!hasTextBody && !toolUses.length) return null;
1604
2653
  }
1605
2654
  else if (!toolUses.length) return null;
@@ -1674,14 +2723,10 @@
1674
2723
  }
1675
2724
 
1676
2725
  if (evType === 'summary') {
1677
- const div = document.createElement('div');
1678
- div.className = 'review-msg summary';
1679
- if (evt.data?.parentUuid) div.dataset.parentUuid = evt.data.parentUuid;
1680
- const body = document.createElement('div');
1681
- body.className = 'msg-text';
1682
- body.textContent = evt.data?.text || '';
1683
- div.appendChild(body);
1684
- return div;
2726
+ if (_isLowValueSystemSummary(evt, text, toolUses)) {
2727
+ return MR.createConversationSystemGroup(evt, time);
2728
+ }
2729
+ return _buildConversationSummaryRow(evt, time, { roleClass: 'system', label: 'System' });
1685
2730
  }
1686
2731
 
1687
2732
  return null;
@@ -1694,13 +2739,14 @@
1694
2739
  }
1695
2740
 
1696
2741
  function _conversationEventText(evt) {
1697
- return String(evt && evt.data && evt.data.text || '').trim();
2742
+ return _conversationEventTextValue(evt);
1698
2743
  }
1699
2744
 
1700
2745
  MR.isConversationPromptEvent = function (evt) {
1701
2746
  if (_conversationEventType(evt) !== 'user') return false;
1702
2747
  const text = _conversationEventText(evt);
1703
2748
  if (!text) return false;
2749
+ if (_isSystemOnlyUserEvent(evt, text)) return false;
1704
2750
  if (MR.parseSkillBody(text) || MR.parseLocalCommand(text) || MR.parseToolResult(text)) return false;
1705
2751
  return true;
1706
2752
  };
@@ -1728,6 +2774,98 @@
1728
2774
  return el.classList.contains('conv-warning-group') && el.classList.contains('codex-warning-group');
1729
2775
  }
1730
2776
 
2777
+ function _isConversationSystemGroup(el) {
2778
+ if (!el || !el.classList) return false;
2779
+ return el.classList.contains('conv-system-group') && el.classList.contains('system-activity-group');
2780
+ }
2781
+
2782
+ function _isConversationCombinedActivityGroup(el) {
2783
+ if (!el || !el.classList) return false;
2784
+ return el.classList.contains('conv-activity-group') && el.classList.contains('combined-activity-group');
2785
+ }
2786
+
2787
+ function _isFoldableOperationalActivityGroup(el) {
2788
+ return _isConversationCombinedActivityGroup(el) || _isConversationToolActivityGroup(el) || _isConversationSystemGroup(el);
2789
+ }
2790
+
2791
+ function _setConversationActivityGroupLabel(group, text) {
2792
+ if (!group || !group.querySelector) return;
2793
+ const label = group.querySelector('.thought-group-label');
2794
+ if (!label) return;
2795
+ let target = label.querySelector('[data-activity-label]');
2796
+ if (!target) {
2797
+ target = Array.from(label.children || []).find((child) => {
2798
+ if (!child || !child.classList) return false;
2799
+ return !child.classList.contains('thought-chevron') && !child.classList.contains('thought-group-count');
2800
+ });
2801
+ }
2802
+ if (!target && typeof document !== 'undefined') {
2803
+ target = document.createElement('span');
2804
+ target.setAttribute('data-activity-label', 'true');
2805
+ const count = label.querySelector('.thought-group-count');
2806
+ label.insertBefore(target, count || null);
2807
+ }
2808
+ if (target) {
2809
+ target.setAttribute('data-activity-label', 'true');
2810
+ target.textContent = text;
2811
+ }
2812
+ }
2813
+
2814
+ function _markConversationCombinedActivityGroup(group) {
2815
+ if (!group || !group.classList) return group;
2816
+ group.classList.add('conv-activity-group');
2817
+ group.classList.add('combined-activity-group');
2818
+ group.classList.add('tool-activity-group');
2819
+ group.dataset.activityKind = 'combined';
2820
+ _setConversationActivityGroupLabel(group, 'Activity');
2821
+ return group;
2822
+ }
2823
+
2824
+ MR.refreshConversationCombinedActivityGroup = function (group) {
2825
+ if (!group || !group.querySelector) return;
2826
+ _markConversationCombinedActivityGroup(group);
2827
+ const items = group.querySelector('.thought-group-items');
2828
+ if (!items) return;
2829
+ const rows = Array.from(items.children || []);
2830
+ const countEl = group.querySelector('.thought-group-count');
2831
+ if (countEl) {
2832
+ const itemCount = rows.length;
2833
+ countEl.textContent = '— ' + itemCount + ' item' + (itemCount === 1 ? '' : 's');
2834
+ }
2835
+ const toolNames = [];
2836
+ const previews = [];
2837
+ let systemCount = 0;
2838
+ let toolResultCount = 0;
2839
+ for (const row of rows) {
2840
+ if (!row || !row.classList) continue;
2841
+ if (row.classList.contains('system-activity-item')) systemCount += 1;
2842
+ if (row.classList.contains('tool-result-item')) toolResultCount += 1;
2843
+ const rawTools = row.dataset?.toolNames || '';
2844
+ if (rawTools) {
2845
+ for (const name of rawTools.split(',')) {
2846
+ const clean = name.trim();
2847
+ if (clean) toolNames.push(clean);
2848
+ }
2849
+ }
2850
+ const preview = row.dataset?.activityPreview || row.dataset?.systemPreview || '';
2851
+ if (preview && preview !== 'No details recorded') previews.push(preview);
2852
+ }
2853
+ const summaryParts = [];
2854
+ const toolSummary = _summarizeNames(toolNames);
2855
+ if (toolSummary) summaryParts.push(toolSummary);
2856
+ if (!toolSummary && toolResultCount) summaryParts.push(toolResultCount + ' tool result' + (toolResultCount === 1 ? '' : 's'));
2857
+ if (systemCount) summaryParts.push(systemCount + ' system event' + (systemCount === 1 ? '' : 's'));
2858
+ if (!summaryParts.length && previews.length) summaryParts.push(previews[0]);
2859
+ const previewEl = group.querySelector('.thought-group-preview');
2860
+ if (previewEl) previewEl.textContent = summaryParts.join(' · ') || 'Activity';
2861
+ const timeEl = group.querySelector('.thought-group-time');
2862
+ if (timeEl) {
2863
+ const f = group.dataset.firstTime || rows[0]?.dataset?.time || '';
2864
+ const l = group.dataset.lastTime || rows[rows.length - 1]?.dataset?.time || '';
2865
+ timeEl.textContent = (f && l && f !== l) ? f + ' – ' + l : (l || f);
2866
+ }
2867
+ };
2868
+
1731
2869
  function _nodeWithin(root, node) {
1732
2870
  if (!root || !node) return false;
1733
2871
  const el = node.nodeType === 1 ? node : node.parentNode;
@@ -1741,9 +2879,21 @@
1741
2879
  return _nodeWithin(root, sel.anchorNode) || _nodeWithin(root, sel.focusNode);
1742
2880
  };
1743
2881
 
2882
+ MR.isPromptTurnPromptTextTarget = function (ev, headerEl) {
2883
+ if (!ev || !headerEl || !ev.target || !ev.target.closest) return false;
2884
+ const textEl = ev.target.closest('.prompt-turn-prompt .msg-text');
2885
+ return !!(textEl && headerEl.contains(textEl));
2886
+ };
2887
+
1744
2888
  MR.shouldKeepPromptTextSelection = function (ev, headerEl) {
2889
+ if (!MR.isPromptTurnPromptTextTarget(ev, headerEl)) return false;
2890
+ return MR.hasTextSelectionInside(headerEl);
2891
+ };
2892
+
2893
+ MR.shouldIgnorePromptTurnHeaderToggle = function (ev, headerEl) {
1745
2894
  if (!ev || !headerEl || !ev.target || !ev.target.closest) return false;
1746
- if (!ev.target.closest('.prompt-turn-prompt .msg-text')) return false;
2895
+ if (ev.target.closest('a,button,input,textarea,select')) return true;
2896
+ if (MR.shouldKeepPromptTextSelection(ev, headerEl)) return true;
1747
2897
  return MR.hasTextSelectionInside(headerEl);
1748
2898
  };
1749
2899
 
@@ -1754,31 +2904,41 @@
1754
2904
  let targetKind = null;
1755
2905
  let merged = false;
1756
2906
  for (const child of Array.from(body.children || [])) {
1757
- const kind = _isConversationToolActivityGroup(child) ? 'tool' : (_isConversationWarningGroup(child) ? 'warning' : null);
1758
- if (!kind) continue;
1759
- if (!target) {
1760
- target = child;
1761
- targetKind = kind;
2907
+ if (!_isFoldableOperationalActivityGroup(child)) {
2908
+ if (_isConversationWarningGroup(child)) {
2909
+ target = null;
2910
+ targetKind = null;
2911
+ }
1762
2912
  continue;
1763
2913
  }
1764
- if (kind !== targetKind) {
2914
+ const childKind = _isConversationCombinedActivityGroup(child)
2915
+ ? 'activity'
2916
+ : (_isConversationToolActivityGroup(child) ? 'tool' : 'system');
2917
+ if (!target) {
1765
2918
  target = child;
1766
- targetKind = kind;
2919
+ targetKind = childKind;
1767
2920
  continue;
1768
2921
  }
1769
2922
  const targetItems = target.querySelector('.thought-group-items');
1770
2923
  const childItems = child.querySelector('.thought-group-items');
1771
2924
  if (!targetItems || !childItems) continue;
2925
+ const shouldPromote = targetKind === 'activity' || childKind === 'activity' || childKind !== targetKind;
1772
2926
  const keepExpanded = target.classList.contains('expanded') || child.classList.contains('expanded');
1773
2927
  while (childItems.firstChild) targetItems.appendChild(childItems.firstChild);
1774
2928
  const childLast = child.dataset && child.dataset.lastTime;
1775
2929
  if (childLast) target.dataset.lastTime = childLast;
1776
2930
  if (keepExpanded && target.classList && !target.classList.contains('expanded')) target.classList.add('expanded');
1777
2931
  child.remove();
2932
+ if (shouldPromote) {
2933
+ _markConversationCombinedActivityGroup(target);
2934
+ targetKind = 'activity';
2935
+ }
1778
2936
  merged = true;
1779
2937
  }
1780
- if (target && targetKind === 'warning' && typeof MR.refreshConversationWarningGroup === 'function') {
1781
- MR.refreshConversationWarningGroup(target);
2938
+ if (target && _isConversationCombinedActivityGroup(target) && typeof MR.refreshConversationCombinedActivityGroup === 'function') {
2939
+ MR.refreshConversationCombinedActivityGroup(target);
2940
+ } else if (target && _isConversationSystemGroup(target) && typeof MR.refreshConversationSystemGroup === 'function') {
2941
+ MR.refreshConversationSystemGroup(target);
1782
2942
  } else if (target && typeof MR.refreshConversationActivityGroup === 'function') {
1783
2943
  MR.refreshConversationActivityGroup(target);
1784
2944
  }
@@ -1810,13 +2970,29 @@
1810
2970
  if (responseBlocks) appendBadge(responseBlocks + ' detail' + (responseBlocks === 1 ? '' : 's'));
1811
2971
  else appendBadge('no response yet');
1812
2972
  if (toolBlocks) appendBadge(toolBlocks + ' tool block' + (toolBlocks === 1 ? '' : 's'));
2973
+ // Token chip: assistant rows stamp data-usage-tokens at render time.
2974
+ // (Guarded: rendering test harnesses stub the DOM without selectors.)
2975
+ let usageTotal = 0;
2976
+ const usageTitles = [];
2977
+ let usageEls = [];
2978
+ try { usageEls = Array.from(body.querySelectorAll('[data-usage-tokens]') || []); } catch { usageEls = []; }
2979
+ for (const el of usageEls) {
2980
+ usageTotal += Number(el.dataset && el.dataset.usageTokens) || 0;
2981
+ if (el.dataset && el.dataset.usageTitle) usageTitles.push(el.dataset.usageTitle);
2982
+ }
2983
+ if (usageTotal > 0) {
2984
+ const chip = document.createElement('span');
2985
+ chip.className = 'prompt-turn-badge token-chip';
2986
+ chip.textContent = _fmtTokens(usageTotal) + ' tok';
2987
+ chip.title = usageTitles.join('\n');
2988
+ meta.appendChild(chip);
2989
+ }
1813
2990
  };
1814
2991
 
1815
2992
  function _wirePromptTurnHeader(turnEl, header) {
1816
2993
  if (!turnEl || !header) return;
1817
2994
  const toggle = (ev) => {
1818
- if (ev && ev.target && ev.target.closest && ev.target.closest('a,button,input,textarea,select')) return;
1819
- if (MR.shouldKeepPromptTextSelection(ev, header)) return;
2995
+ if (MR.shouldIgnorePromptTurnHeaderToggle(ev, header)) return;
1820
2996
  MR.setPromptTurnExpanded(turnEl, !turnEl.classList.contains('expanded'));
1821
2997
  };
1822
2998
  header.addEventListener('click', toggle);
@@ -1904,19 +3080,31 @@
1904
3080
  // The body uses the same 1500-char / 30-line threshold as Review so the
1905
3081
  // two views feel identical for long messages — closes the 3000-char hard
1906
3082
  // truncate gap that Conversation had pre-Phase-2.
1907
- function _appendTextWithExpand(host, text) {
1908
- const needsCollapse = text.length > 1500 || text.split('\n').length > 30;
3083
+ function _appendTextWithExpand(host, text, message, opts) {
3084
+ opts = opts || {};
3085
+ const displayText = _messageTextForDisplay(text);
3086
+ const attachmentMessage = Object.assign({}, message || {}, { text: text || '' });
3087
+ const attachmentHtml = MR.renderImageAttachmentsHtml(attachmentMessage);
3088
+ const needsCollapse = displayText.length > 1500 || displayText.split('\n').length > 30;
1909
3089
  // formatMsgText already runs DOMPurify; we re-sanitize at the assignment
1910
3090
  // site to match Review's defense-in-depth pattern. No untrusted HTML
1911
3091
  // ever reaches innerHTML without sanitization.
3092
+ const rendered = displayText ? MR.formatMsgText(displayText) : (opts.emptyHtml || '');
1912
3093
  const safe = (typeof DOMPurify !== 'undefined')
1913
- ? DOMPurify.sanitize(MR.formatMsgText(text))
1914
- : MR.formatMsgText(text);
3094
+ ? DOMPurify.sanitize(rendered)
3095
+ : rendered;
1915
3096
  const body = document.createElement('div');
1916
3097
  body.className = 'msg-text' + (needsCollapse ? ' collapsed' : '');
1917
3098
  body.innerHTML = safe;
1918
- if (!_hasMeaningfulMarkdownContent(body)) return false;
3099
+ if (!_hasMeaningfulMarkdownContent(body) && !attachmentHtml) return false;
1919
3100
  host.appendChild(body);
3101
+ if (attachmentHtml) {
3102
+ const template = document.createElement('template');
3103
+ template.innerHTML = (typeof DOMPurify !== 'undefined')
3104
+ ? DOMPurify.sanitize(attachmentHtml, { ADD_ATTR: _IMAGE_ATTACHMENT_SAFE_ATTRS })
3105
+ : attachmentHtml;
3106
+ while (template.content.firstChild) host.appendChild(template.content.firstChild);
3107
+ }
1920
3108
  if (needsCollapse) {
1921
3109
  const btn = document.createElement('button');
1922
3110
  btn.className = 'msg-expand visible';