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
@@ -37,6 +37,9 @@ const _streamState = {
37
37
  // _pendingEvents: sessionId → [stream-event...] buffered during prime.
38
38
  // _parentUuidSeen: sessionId → Set of parentUuids already rendered,
39
39
  // used to dedup stream-events that overlap with primed history.
40
+ // _messageFingerprintSeen: sessionId → Set of semantic message keys
41
+ // rendered from HTTP history or live stream. This reconciles the two
42
+ // data planes so stream-init cannot replay rows already in the snapshot.
40
43
  _primed: new Map(),
41
44
  _primedTarget: new Map(),
42
45
  _priming: new Map(),
@@ -44,6 +47,7 @@ const _streamState = {
44
47
  _primeToken: new Map(),
45
48
  _pendingEvents: new Map(),
46
49
  _parentUuidSeen: new Map(),
50
+ _messageFingerprintSeen: new Map(),
47
51
  };
48
52
 
49
53
  function _normalizeSessionViewMode(view) {
@@ -132,7 +136,7 @@ function _tooltipPhaseLabel(phase) {
132
136
  if (key === 'implementing') return 'Implementing';
133
137
  if (key === 'investigating') return 'Investigating';
134
138
  if (key === 'verifying') return 'Verifying';
135
- if (key === 'waiting') return 'Waiting';
139
+ if (key === 'waiting') return 'Needs You';
136
140
  if (key === 'running') return 'Running';
137
141
  return key ? _tooltipStatusLabel(key) : 'Progress';
138
142
  }
@@ -496,8 +500,19 @@ function renderConversationEvent(evt) {
496
500
  return MR.renderConversationEvent(evt);
497
501
  }
498
502
 
503
+ function _linkConversationDocumentReferences(container, root) {
504
+ if (!container || !root || !window.CTMDocLinks || typeof window.CTMDocLinks.linkifyElement !== 'function') return;
505
+ try {
506
+ const sessionId = container.dataset?.sessionId || '';
507
+ window.CTMDocLinks.linkifyElement(root, window.CTMDocLinks.contextForSession(sessionId));
508
+ } catch (e) {
509
+ console.warn('[stream-view] document linkification failed:', e && e.message ? e.message : e);
510
+ }
511
+ }
512
+
499
513
  // When the new element AND the last existing child are compatible collapsible
500
- // operational rows (`.conv-tool-group` or `.conv-warning-group`), fold the new
514
+ // operational rows (`.conv-tool-group`, `.conv-warning-group`, or
515
+ // `.conv-system-group`), fold the new
501
516
  // one into the previous one's items list and bump the count + last-time.
502
517
  // Result: consecutive low-value activity rows render as ONE thought-group,
503
518
  // matching Review's `groupMessages` behavior. Returns true if merged (caller
@@ -505,12 +520,61 @@ function renderConversationEvent(evt) {
505
520
  function _mergeConsecutiveToolGroups(container, newEl) {
506
521
  const groupKind = (el) => {
507
522
  if (!el || !el.classList) return null;
523
+ if (el.classList.contains('conv-activity-group')) return 'activity';
508
524
  if (el.classList.contains('conv-tool-group')) return 'tool';
509
525
  if (el.classList.contains('conv-warning-group')) return 'warning';
526
+ if (el.classList.contains('conv-system-group')) return 'system';
510
527
  return null;
511
528
  };
529
+ const foldableKind = (kind) => kind === 'activity' || kind === 'tool' || kind === 'system';
530
+ const markCombined = (el) => {
531
+ if (!el || !el.classList) return;
532
+ el.classList.add('conv-activity-group');
533
+ el.classList.add('combined-activity-group');
534
+ el.classList.add('tool-activity-group');
535
+ if (el.dataset) el.dataset.activityKind = 'combined';
536
+ };
537
+ const refreshGroup = (el, kind) => {
538
+ if (kind === 'activity' && typeof MR !== 'undefined' && typeof MR.refreshConversationCombinedActivityGroup === 'function') {
539
+ MR.refreshConversationCombinedActivityGroup(el);
540
+ } else if (kind === 'warning' && typeof MR !== 'undefined' && typeof MR.refreshConversationWarningGroup === 'function') {
541
+ MR.refreshConversationWarningGroup(el);
542
+ } else if (kind === 'system' && typeof MR !== 'undefined' && typeof MR.refreshConversationSystemGroup === 'function') {
543
+ MR.refreshConversationSystemGroup(el);
544
+ } else if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
545
+ MR.refreshConversationActivityGroup(el);
546
+ }
547
+ };
512
548
  const kind = groupKind(newEl);
513
549
  if (!kind) return false;
550
+ if (foldableKind(kind)) {
551
+ const lastChild = container.lastElementChild;
552
+ const lastKind = groupKind(lastChild);
553
+ if (!foldableKind(lastKind)) return false;
554
+ const targetItems = lastChild.querySelector('.thought-group-items');
555
+ const newItems = newEl.querySelector('.thought-group-items');
556
+ if (!targetItems || !newItems) return false;
557
+ while (newItems.firstChild) targetItems.appendChild(newItems.firstChild);
558
+ const newLast = newEl.dataset.lastTime || '';
559
+ if (newLast) lastChild.dataset.lastTime = newLast;
560
+ const shouldPromote = lastKind === 'activity' || kind === 'activity' || lastKind !== kind;
561
+ if (shouldPromote) markCombined(lastChild);
562
+ refreshGroup(lastChild, shouldPromote ? 'activity' : lastKind);
563
+ return true;
564
+ }
565
+ if (kind === 'warning') {
566
+ const existingWarning = container.querySelector(':scope > .conv-warning-group');
567
+ if (existingWarning) {
568
+ const targetItems = existingWarning.querySelector('.thought-group-items');
569
+ const newItems = newEl.querySelector('.thought-group-items');
570
+ if (!targetItems || !newItems) return false;
571
+ while (newItems.firstChild) targetItems.appendChild(newItems.firstChild);
572
+ const newLast = newEl.dataset.lastTime || '';
573
+ if (newLast) existingWarning.dataset.lastTime = newLast;
574
+ refreshGroup(existingWarning, 'warning');
575
+ return true;
576
+ }
577
+ }
514
578
  const lastChild = container.lastElementChild;
515
579
  if (groupKind(lastChild) !== kind) return false;
516
580
  const targetItems = lastChild.querySelector('.thought-group-items');
@@ -522,17 +586,18 @@ function _mergeConsecutiveToolGroups(container, newEl) {
522
586
  // rows prevents a later update from replacing the whole merged group.
523
587
  const newLast = newEl.dataset.lastTime || '';
524
588
  if (newLast) lastChild.dataset.lastTime = newLast;
525
- if (kind === 'warning' && typeof MR !== 'undefined' && typeof MR.refreshConversationWarningGroup === 'function') {
526
- MR.refreshConversationWarningGroup(lastChild);
527
- } else if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
528
- MR.refreshConversationActivityGroup(lastChild);
529
- }
589
+ refreshGroup(lastChild, kind);
530
590
  return true;
531
591
  }
532
592
 
533
593
  function _assignConversationParentUuid(el, parentUuid) {
534
594
  if (!el || !parentUuid) return;
535
- if (el.classList && (el.classList.contains('conv-tool-group') || el.classList.contains('conv-warning-group'))) {
595
+ if (el.classList && (
596
+ el.classList.contains('conv-activity-group') ||
597
+ el.classList.contains('conv-tool-group') ||
598
+ el.classList.contains('conv-warning-group') ||
599
+ el.classList.contains('conv-system-group')
600
+ )) {
536
601
  const items = el.querySelector('.thought-group-items');
537
602
  const row = items && (items.lastElementChild || (items.children && items.children[items.children.length - 1]));
538
603
  if (row && row.dataset && !row.dataset.parentUuid) row.dataset.parentUuid = parentUuid;
@@ -554,11 +619,38 @@ function _replaceConversationParentEvent(existing, newEl) {
554
619
  else existing.remove();
555
620
  return true;
556
621
  }
557
- const existingGroup = existing.closest ? (existing.closest('.conv-tool-group') || existing.closest('.conv-warning-group')) : null;
558
- const existingKind = existingGroup && existingGroup.classList.contains('conv-warning-group') ? 'warning' : 'tool';
559
- const newKind = newEl && newEl.classList && newEl.classList.contains('conv-warning-group') ? 'warning' : 'tool';
560
- if (existingGroup && newEl && newEl.classList && (newEl.classList.contains('conv-tool-group') || newEl.classList.contains('conv-warning-group')) && existingKind === newKind) {
561
- const targetRow = existing.closest('.tool-activity-item') || existing;
622
+ const conversationGroupKind = (el) => {
623
+ if (!el || !el.classList) return '';
624
+ if (el.classList.contains('conv-activity-group')) return 'activity';
625
+ if (el.classList.contains('conv-warning-group')) return 'warning';
626
+ if (el.classList.contains('conv-system-group')) return 'system';
627
+ if (el.classList.contains('conv-tool-group')) return 'tool';
628
+ return '';
629
+ };
630
+ const refreshConversationGroup = (group, kind) => {
631
+ if (!group || typeof MR === 'undefined') return;
632
+ if (kind === 'activity' && typeof MR.refreshConversationCombinedActivityGroup === 'function') {
633
+ MR.refreshConversationCombinedActivityGroup(group);
634
+ } else if (kind === 'warning' && typeof MR.refreshConversationWarningGroup === 'function') {
635
+ MR.refreshConversationWarningGroup(group);
636
+ } else if (kind === 'system' && typeof MR.refreshConversationSystemGroup === 'function') {
637
+ MR.refreshConversationSystemGroup(group);
638
+ } else if (typeof MR.refreshConversationActivityGroup === 'function') {
639
+ MR.refreshConversationActivityGroup(group);
640
+ }
641
+ };
642
+ const compatibleActivityReplacement = (existingKind, incomingKind) => {
643
+ if (existingKind === incomingKind) return true;
644
+ if (existingKind !== 'activity') return false;
645
+ return incomingKind === 'tool' || incomingKind === 'system' || incomingKind === 'activity';
646
+ };
647
+ const existingGroup = existing.closest
648
+ ? (existing.closest('.conv-activity-group') || existing.closest('.conv-tool-group') || existing.closest('.conv-warning-group') || existing.closest('.conv-system-group'))
649
+ : null;
650
+ const existingKind = conversationGroupKind(existingGroup);
651
+ const newKind = conversationGroupKind(newEl);
652
+ if (existingGroup && newEl && newEl.classList && newKind && compatibleActivityReplacement(existingKind, newKind)) {
653
+ const targetRow = existing.closest('.tool-activity-item') || existing.closest('.system-activity-item') || existing;
562
654
  const targetParent = targetRow.parentNode;
563
655
  const newItems = newEl.querySelector('.thought-group-items');
564
656
  if (!targetParent || !newItems) return false;
@@ -566,25 +658,17 @@ function _replaceConversationParentEvent(existing, newEl) {
566
658
  for (const row of rows) targetParent.insertBefore(row, targetRow);
567
659
  targetParent.removeChild(targetRow);
568
660
  if (newEl.dataset.lastTime) existingGroup.dataset.lastTime = newEl.dataset.lastTime;
569
- if (existingKind === 'warning' && typeof MR !== 'undefined' && typeof MR.refreshConversationWarningGroup === 'function') {
570
- MR.refreshConversationWarningGroup(existingGroup);
571
- } else if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
572
- MR.refreshConversationActivityGroup(existingGroup);
573
- }
661
+ refreshConversationGroup(existingGroup, existingKind);
574
662
  refreshTurn();
575
663
  return true;
576
664
  }
577
665
  if (existingGroup && !newEl) {
578
- const targetRow = existing.closest('.tool-activity-item') || existing;
666
+ const targetRow = existing.closest('.tool-activity-item') || existing.closest('.system-activity-item') || existing;
579
667
  const targetParent = targetRow.parentNode;
580
668
  if (targetParent) targetParent.removeChild(targetRow);
581
669
  const remaining = existingGroup.querySelector('.thought-group-items')?.children?.length || 0;
582
670
  if (remaining === 0) existingGroup.remove();
583
- else if (existingKind === 'warning' && typeof MR !== 'undefined' && typeof MR.refreshConversationWarningGroup === 'function') {
584
- MR.refreshConversationWarningGroup(existingGroup);
585
- } else if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
586
- MR.refreshConversationActivityGroup(existingGroup);
587
- }
671
+ else refreshConversationGroup(existingGroup, existingKind);
588
672
  refreshTurn();
589
673
  return true;
590
674
  }
@@ -594,6 +678,96 @@ function _replaceConversationParentEvent(existing, newEl) {
594
678
  return true;
595
679
  }
596
680
 
681
+ function _conversationScrollNearBottom(container) {
682
+ if (!container) return true;
683
+ const remaining = Number(container.scrollHeight || 0)
684
+ - Number(container.scrollTop || 0)
685
+ - Number(container.clientHeight || 0);
686
+ return remaining < 100;
687
+ }
688
+
689
+ function _conversationScrollAnchorCandidates(container) {
690
+ if (!container) return [];
691
+ const selector = [
692
+ ':scope > .prompt-turn',
693
+ ':scope > .review-msg',
694
+ ':scope > .thought-group',
695
+ ':scope > .conversation-state',
696
+ ':scope > .conversation-load-older',
697
+ ':scope .prompt-turn-header',
698
+ ':scope .review-msg',
699
+ ':scope .thought-group',
700
+ ':scope .tool-activity-item',
701
+ ':scope .system-activity-item',
702
+ ':scope .msg-text',
703
+ ].join(', ');
704
+ if (typeof container.querySelectorAll === 'function') {
705
+ try { return Array.from(container.querySelectorAll(selector)); } catch {}
706
+ }
707
+ return Array.from(container.children || []);
708
+ }
709
+
710
+ function _captureConversationScrollAnchor(container) {
711
+ if (!container) return null;
712
+ const followBottom = _conversationScrollNearBottom(container);
713
+ const anchor = {
714
+ followBottom,
715
+ scrollTop: Number(container.scrollTop || 0),
716
+ scrollHeight: Number(container.scrollHeight || 0),
717
+ element: null,
718
+ topOffset: 0,
719
+ };
720
+ if (followBottom) return anchor;
721
+ let containerRect = null;
722
+ try { containerRect = container.getBoundingClientRect && container.getBoundingClientRect(); } catch {}
723
+ if (!containerRect) return anchor;
724
+ const top = Number(containerRect.top || 0);
725
+ const bottom = top + Number(container.clientHeight || containerRect.height || 0);
726
+ for (const el of _conversationScrollAnchorCandidates(container)) {
727
+ if (!el || typeof el.getBoundingClientRect !== 'function') continue;
728
+ let rect = null;
729
+ try { rect = el.getBoundingClientRect(); } catch {}
730
+ if (!rect || Number(rect.height || 0) <= 0) continue;
731
+ if (Number(rect.bottom || 0) <= top + 2) continue;
732
+ if (Number(rect.top || 0) >= bottom - 2) continue;
733
+ anchor.element = el;
734
+ anchor.topOffset = Number(rect.top || 0) - top;
735
+ break;
736
+ }
737
+ return anchor;
738
+ }
739
+
740
+ function _restoreConversationScrollAnchor(container, anchor) {
741
+ if (!container || !anchor) return;
742
+ if (anchor.followBottom) {
743
+ container.scrollTop = container.scrollHeight;
744
+ return;
745
+ }
746
+ const fallback = () => { container.scrollTop = anchor.scrollTop; };
747
+ const el = anchor.element;
748
+ if (!el || el.isConnected === false || typeof el.getBoundingClientRect !== 'function') {
749
+ fallback();
750
+ return;
751
+ }
752
+ let containerRect = null;
753
+ let rect = null;
754
+ try {
755
+ containerRect = container.getBoundingClientRect && container.getBoundingClientRect();
756
+ rect = el.getBoundingClientRect();
757
+ } catch {}
758
+ if (!containerRect || !rect) {
759
+ fallback();
760
+ return;
761
+ }
762
+ const nextOffset = Number(rect.top || 0) - Number(containerRect.top || 0);
763
+ const delta = nextOffset - Number(anchor.topOffset || 0);
764
+ if (Number.isFinite(delta) && Math.abs(delta) > 0.5) {
765
+ container.scrollTop = Number(container.scrollTop || 0) + delta;
766
+ } else {
767
+ container.scrollTop = anchor.scrollTop;
768
+ }
769
+ }
770
+
597
771
  function _isPromptTurnContainer(container) {
598
772
  return !!(container && container.dataset && (container.dataset.turnMode === 'conversation' || container.dataset.turnMode === 'review'));
599
773
  }
@@ -691,6 +865,117 @@ function _removePromptTurnEmpty(body) {
691
865
  }
692
866
  }
693
867
 
868
+ function _conversationPromptTextKey(text) {
869
+ return String(text || '').replace(/\s+/g, ' ').trim();
870
+ }
871
+
872
+ function _conversationEventTimestampMs(evt) {
873
+ const raw = evt && evt.timestamp;
874
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
875
+ if (typeof raw === 'string' && /^\d{10,}$/.test(raw.trim())) {
876
+ const numeric = Number(raw.trim());
877
+ if (Number.isFinite(numeric)) return numeric;
878
+ }
879
+ const parsed = Date.parse(raw || '');
880
+ return Number.isFinite(parsed) ? parsed : 0;
881
+ }
882
+
883
+ function _conversationEventRole(evt) {
884
+ const role = String(evt?.data?.type || evt?.eventType || evt?.type || evt?.role || '').trim();
885
+ return role === 'user' || role === 'assistant' ? role : '';
886
+ }
887
+
888
+ function _conversationEventTextKey(evt) {
889
+ return _conversationPromptTextKey(evt?.data?.text ?? evt?.text ?? evt?.message ?? '');
890
+ }
891
+
892
+ function _conversationEventFingerprint(evt) {
893
+ const role = _conversationEventRole(evt);
894
+ const text = _conversationEventTextKey(evt);
895
+ if (!role || !text) return '';
896
+ const ts = _conversationEventTimestampMs(evt);
897
+ if (!ts) return '';
898
+ return [role, String(ts), text].join('\u001f');
899
+ }
900
+
901
+ function _conversationMessageFingerprintSet(sessionId, create) {
902
+ const id = String(sessionId || '').trim();
903
+ if (!id) return null;
904
+ let seen = _streamState._messageFingerprintSeen.get(id);
905
+ if (!seen && create) {
906
+ seen = new Set();
907
+ _streamState._messageFingerprintSeen.set(id, seen);
908
+ }
909
+ return seen || null;
910
+ }
911
+
912
+ function _markConversationEventSeen(sessionId, evt) {
913
+ const fingerprint = _conversationEventFingerprint(evt);
914
+ if (!fingerprint) return false;
915
+ const seen = _conversationMessageFingerprintSet(sessionId, true);
916
+ if (!seen) return false;
917
+ seen.add(fingerprint);
918
+ return true;
919
+ }
920
+
921
+ function _conversationEventAlreadySeen(sessionId, evt) {
922
+ const fingerprint = _conversationEventFingerprint(evt);
923
+ if (!fingerprint) return false;
924
+ const seen = _conversationMessageFingerprintSet(sessionId, false);
925
+ return !!(seen && seen.has(fingerprint));
926
+ }
927
+
928
+ function _conversationStampPromptTurn(turn, evt) {
929
+ if (!turn || !turn.dataset || !evt || !MR || typeof MR.isConversationPromptEvent !== 'function') return;
930
+ if (!MR.isConversationPromptEvent(evt)) return;
931
+ const key = _conversationEventTextKey(evt);
932
+ if (key) turn.dataset.promptTextKey = key;
933
+ const ts = _conversationEventTimestampMs(evt);
934
+ if (ts) turn.dataset.promptTimestampMs = String(ts);
935
+ }
936
+
937
+ function _conversationPromptTurnTextKey(turn) {
938
+ if (!turn || !turn.querySelector) return '';
939
+ const existing = turn.dataset && turn.dataset.promptTextKey;
940
+ if (existing) return existing;
941
+ const textEl = turn.querySelector('.prompt-turn-prompt .msg-text');
942
+ return _conversationPromptTextKey(textEl ? textEl.textContent : '');
943
+ }
944
+
945
+ function _conversationPromptTurnTimestampMs(turn) {
946
+ if (!turn || !turn.dataset) return 0;
947
+ const stored = Number(turn.dataset.promptTimestampMs || 0);
948
+ if (Number.isFinite(stored) && stored > 0) return stored;
949
+ return 0;
950
+ }
951
+
952
+ function _findDuplicateConversationPromptTurn(container, evt) {
953
+ if (!_isPromptTurnContainer(container)) return null;
954
+ if (!MR || typeof MR.isConversationPromptEvent !== 'function' || !MR.isConversationPromptEvent(evt)) return null;
955
+ const key = _conversationEventTextKey(evt);
956
+ if (!key) return null;
957
+ const ts = _conversationEventTimestampMs(evt);
958
+ const children = Array.from(container.children || []);
959
+ let checkedPromptTurns = 0;
960
+ for (let i = children.length - 1; i >= 0 && checkedPromptTurns < 6; i -= 1) {
961
+ const turn = children[i];
962
+ if (!turn || !turn.classList || !turn.classList.contains('prompt-turn')) continue;
963
+ if (turn.classList.contains('setup-turn')) continue;
964
+ checkedPromptTurns += 1;
965
+ if (_conversationPromptTurnTextKey(turn) !== key) continue;
966
+ const existingTs = _conversationPromptTurnTimestampMs(turn);
967
+ if (ts && existingTs) {
968
+ if (Math.abs(ts - existingTs) <= 5000) return turn;
969
+ continue;
970
+ }
971
+ // Missing timestamps only happen in legacy/import fallback paths. Keep this
972
+ // conservative: dedup only the current tail prompt so a user can still send
973
+ // the same short command again later.
974
+ if (checkedPromptTurns === 1) return turn;
975
+ }
976
+ return null;
977
+ }
978
+
694
979
  function _ensureResponseTurn(container, opts) {
695
980
  let turn = _latestPromptTurn(container);
696
981
  if (turn) return turn;
@@ -705,23 +990,101 @@ function _ensureResponseTurn(container, opts) {
705
990
 
706
991
  function _renderConversationNodeForEvent(evt, opts) {
707
992
  if (MR && typeof MR.isConversationPromptEvent === 'function' && MR.isConversationPromptEvent(evt)) {
708
- return MR.createConversationTurn(evt, { expanded: !!(opts && opts.expandPrompt) });
993
+ const turn = MR.createConversationTurn(evt, { expanded: !!(opts && opts.expandPrompt) });
994
+ _conversationStampPromptTurn(turn, evt);
995
+ return turn;
709
996
  }
710
997
  return renderConversationEvent(evt);
711
998
  }
712
999
 
1000
+ // A streaming assistant turn can be rendered once as a stale-cache PARTIAL (the large-session
1001
+ // stale-cache path serves it without a parentUuid, so the row has no data-parent-uuid) and then
1002
+ // again as the live FINAL. Their text differs (partial vs final), so neither the parentUuid match
1003
+ // nor the exact-fingerprint dedup in _applyStreamEvent catches it and the final appends a
1004
+ // duplicate. When the turn's last assistant text row is a long PREFIX of (or equal to) the
1005
+ // incoming assistant text, they are the same streamed turn: keep the longer copy in its slot
1006
+ // instead of stacking a duplicate. Mirrors the server-side messagesRepresentSameEvent prefix rule.
1007
+ const _STREAM_PREFIX_DEDUP_MIN = 200;
1008
+ function _conversationRowRawText(el) {
1009
+ return el && typeof el._convRawText === 'string' ? el._convRawText : '';
1010
+ }
1011
+ function _supersedeStreamingAssistantPrefix(body, newEl, newText, evt) {
1012
+ if (!body || !newEl || !newEl.classList
1013
+ || !newEl.classList.contains('assistant') || newEl.classList.contains('tool-only')) return null;
1014
+ const prev = body.lastElementChild;
1015
+ if (!prev || !prev.classList || !prev.classList.contains('review-msg')
1016
+ || !prev.classList.contains('assistant') || prev.classList.contains('tool-only')) return null;
1017
+ const a = _conversationRowRawText(prev);
1018
+ const b = String(newText || '');
1019
+ if (a.length < _STREAM_PREFIX_DEDUP_MIN || b.length < _STREAM_PREFIX_DEDUP_MIN) return null;
1020
+ const shorter = a.length <= b.length ? a : b;
1021
+ const longer = a.length <= b.length ? b : a;
1022
+ if (!longer.startsWith(shorter)) return null; // not the same streamed turn — keep both
1023
+ const keep = b.length >= a.length ? newEl : prev;
1024
+ if (keep === newEl) _replaceConversationParentEvent(prev, newEl); // newEl takes the partial's slot
1025
+ keep._convRawText = longer;
1026
+ if (evt && evt.data && evt.data.parentUuid) _assignConversationParentUuid(keep, evt.data.parentUuid);
1027
+ return keep;
1028
+ }
1029
+
1030
+ const _TOOL_CARD_EVENT_KINDS = new Set(['tool_call', 'shell', 'patch', 'web_search']);
1031
+
1032
+ function _findToolCardByCallId(scopeEl, callId) {
1033
+ if (!scopeEl || !callId || typeof scopeEl.querySelector !== 'function') return null;
1034
+ const escaped = (window.CSS && CSS.escape) ? CSS.escape(callId) : callId.replace(/"/g, '\\"');
1035
+ return scopeEl.querySelector('.tool-card[data-call-id="' + escaped + '"]');
1036
+ }
1037
+
1038
+ // Structured tool events against the live DOM:
1039
+ // - a tool_result fills its call's card IN PLACE (matched on data-call-id) —
1040
+ // no new node;
1041
+ // - a replayed tool_call/shell/patch whose card already exists dedups to it.
1042
+ // Returns the affected card element, or null to continue the normal path.
1043
+ function _applyStructuredToolEvent(scopeEl, evt) {
1044
+ if (!MR || typeof MR.messageMeta !== 'function') return null;
1045
+ const meta = MR.messageMeta(evt);
1046
+ if (!meta || !meta.callId) return null;
1047
+ const card = _findToolCardByCallId(scopeEl, meta.callId);
1048
+ if (!card) return null;
1049
+ if (meta.kind === 'tool_result') {
1050
+ // Fill only a pending card; a replayed result against an already-filled
1051
+ // card is a duplicate event — dedup to the existing node.
1052
+ if (card.getAttribute('data-status') === 'pending' && typeof MR.fillToolCardResult === 'function') {
1053
+ MR.fillToolCardResult(card, {
1054
+ text: (evt.data && evt.data.text) || evt.text || '',
1055
+ metadata: meta,
1056
+ timestamp: evt.timestamp,
1057
+ });
1058
+ }
1059
+ return card;
1060
+ }
1061
+ if (_TOOL_CARD_EVENT_KINDS.has(meta.kind)) return card; // dedup replayed call
1062
+ return null;
1063
+ }
1064
+
713
1065
  function _appendConversationEventToTurns(container, evt, opts) {
714
1066
  opts = opts || {};
715
1067
  if (!container || !MR || typeof MR.isConversationPromptEvent !== 'function') return null;
716
1068
  if (MR.isConversationPromptEvent(evt)) {
717
1069
  if (opts.collapseExisting) _collapsePromptTurns(container);
718
1070
  const turn = MR.createConversationTurn(evt, { expanded: !!opts.expandPrompt });
1071
+ _conversationStampPromptTurn(turn, evt);
719
1072
  _removeConversationState(container);
720
1073
  container.appendChild(turn);
1074
+ _linkConversationDocumentReferences(container, turn);
721
1075
  if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
722
1076
  return turn;
723
1077
  }
724
1078
 
1079
+ const filledCard = _applyStructuredToolEvent(container, evt);
1080
+ if (filledCard) {
1081
+ if (typeof MR.refreshPromptTurnMeta === 'function') {
1082
+ const cardTurn = filledCard.closest ? filledCard.closest('.prompt-turn') : null;
1083
+ if (cardTurn) MR.refreshPromptTurnMeta(cardTurn);
1084
+ }
1085
+ return filledCard;
1086
+ }
1087
+
725
1088
  const el = renderConversationEvent(evt);
726
1089
  if (!el) return null;
727
1090
  _removeConversationState(container);
@@ -729,8 +1092,18 @@ function _appendConversationEventToTurns(container, evt, opts) {
729
1092
  const body = MR.getPromptTurnBody(turn);
730
1093
  if (!body) return null;
731
1094
  _removePromptTurnEmpty(body);
1095
+ const rawText = (evt.data && evt.data.text) || evt.text || '';
1096
+ el._convRawText = rawText;
732
1097
  if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
733
- if (!_mergeConsecutiveToolGroups(body, el)) body.appendChild(el);
1098
+ const superseded = _supersedeStreamingAssistantPrefix(body, el, rawText, evt);
1099
+ if (superseded) {
1100
+ _linkConversationDocumentReferences(container, body);
1101
+ if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
1102
+ return superseded;
1103
+ }
1104
+ const merged = _mergeConsecutiveToolGroups(body, el);
1105
+ if (!merged) body.appendChild(el);
1106
+ _linkConversationDocumentReferences(container, merged ? body : el);
734
1107
  if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
735
1108
  return el;
736
1109
  }
@@ -738,6 +1111,7 @@ function _appendConversationEventToTurns(container, evt, opts) {
738
1111
  function populateConversationView(container, events) {
739
1112
  container.textContent = '';
740
1113
  if (container.dataset) container.dataset.turnMode = container.dataset.turnMode || 'conversation';
1114
+ const sessionId = container.dataset?.sessionId || '';
741
1115
  if (!events || events.length === 0) {
742
1116
  _renderConversationState(container, 'No transcript messages found for this session.');
743
1117
  container.scrollTop = container.scrollHeight;
@@ -745,15 +1119,26 @@ function populateConversationView(container, events) {
745
1119
  }
746
1120
  for (const evt of events) {
747
1121
  if (_isPromptTurnContainer(container)) {
748
- _appendConversationEventToTurns(container, evt, { expandPrompt: false, collapseExisting: false, expandSetup: false });
1122
+ if (_appendConversationEventToTurns(container, evt, { expandPrompt: false, collapseExisting: false, expandSetup: false })) {
1123
+ _markConversationEventSeen(sessionId, evt);
1124
+ }
1125
+ continue;
1126
+ }
1127
+ if (_applyStructuredToolEvent(container, evt)) {
1128
+ _markConversationEventSeen(sessionId, evt);
749
1129
  continue;
750
1130
  }
751
1131
  const el = renderConversationEvent(evt);
752
1132
  if (!el) continue;
753
1133
  if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
754
- if (_mergeConsecutiveToolGroups(container, el)) continue;
1134
+ if (_mergeConsecutiveToolGroups(container, el)) {
1135
+ _markConversationEventSeen(sessionId, evt);
1136
+ continue;
1137
+ }
755
1138
  container.appendChild(el);
1139
+ _markConversationEventSeen(sessionId, evt);
756
1140
  }
1141
+ _linkConversationDocumentReferences(container, container);
757
1142
  if (_isPromptTurnContainer(container)) _refreshLatestPromptTurnStatus(container.dataset?.sessionId || '', container);
758
1143
  container.scrollTop = container.scrollHeight;
759
1144
  }
@@ -912,12 +1297,37 @@ let _streamStatusRefreshInFlight = null;
912
1297
  let _streamStatusLastRefreshAt = 0;
913
1298
  let _streamStatusRenderTimer = null;
914
1299
 
1300
+ function _clearStreamRestoreStateFallback(s) {
1301
+ if (!s) return;
1302
+ s._restoreDeferredStarting = false;
1303
+ s._restoreStarting = false;
1304
+ s._restoreResuming = false;
1305
+ s._restoreStatus = '';
1306
+ if (s.meta) {
1307
+ s.meta.restoreStarting = false;
1308
+ s.meta.restore_starting = false;
1309
+ s.meta.restoreResuming = false;
1310
+ s.meta.restore_resuming = false;
1311
+ s.meta.restoreStatus = '';
1312
+ s.meta.restore_status = '';
1313
+ }
1314
+ }
1315
+
1316
+ function _clearStreamRestoreState(s, root) {
1317
+ if (root && typeof root._ctmClearSessionRestoreState === 'function') {
1318
+ root._ctmClearSessionRestoreState(s);
1319
+ return;
1320
+ }
1321
+ _clearStreamRestoreStateFallback(s);
1322
+ }
1323
+
915
1324
  function applyStreamStatus(msg) {
916
1325
  const status = msg && (msg.newStatus || msg.status);
917
1326
  const ctmId = msg && (msg.ctmSessionId || msg.sessionId);
918
1327
  if (!ctmId || !status || typeof state === 'undefined' || !state.sessions) return false;
919
1328
  const s = state.sessions.get(ctmId);
920
1329
  if (!s) return false;
1330
+ const root = typeof window !== 'undefined' ? window : globalThis;
921
1331
 
922
1332
  const beforeEffectiveStatus = typeof getSessionStatus === 'function'
923
1333
  ? getSessionStatus(s).cls
@@ -928,8 +1338,11 @@ function applyStreamStatus(msg) {
928
1338
  const normalizedStatus = typeof normalizeLiveSessionStatus === 'function'
929
1339
  ? normalizeLiveSessionStatus(status)
930
1340
  : String(status || '').toLowerCase();
1341
+ const serverReady = !!(root && root._ctmState && root._ctmState._serverReady);
1342
+ if (normalizedStatus === 'running' || (serverReady && normalizedStatus && normalizedStatus !== 'resuming')) {
1343
+ _clearStreamRestoreState(s, root);
1344
+ }
931
1345
  if (normalizedStatus && normalizedStatus !== 'running') {
932
- const root = typeof window !== 'undefined' ? window : globalThis;
933
1346
  const heldNonRunningStatus = root && typeof root._ctmRecordClientCodexPendingNonRunning === 'function'
934
1347
  ? root._ctmRecordClientCodexPendingNonRunning(
935
1348
  s,
@@ -1037,10 +1450,17 @@ if (_streamStatusPollTimer && typeof _streamStatusPollTimer.unref === 'function'
1037
1450
 
1038
1451
  // --- View Toggle ---
1039
1452
 
1040
- function createViewToggle(sessionId) {
1041
- const controls = document.createElement('div');
1042
- controls.className = 'stream-view-controls';
1453
+ // The Terminal/Conversation toggle only makes sense when the agent has a
1454
+ // structured transcript (Claude/Codex/Wall-E). Plain shells and terminal-only
1455
+ // agents have no conversation view, so the toggle would be a dead control.
1456
+ function _sessionHasConversationView(sessionId) {
1457
+ const caps = (typeof window !== 'undefined' && typeof window._ctmAgentCapsForSession === 'function')
1458
+ ? window._ctmAgentCapsForSession(sessionId)
1459
+ : null;
1460
+ return !caps || caps.structuredTranscript !== false;
1461
+ }
1043
1462
 
1463
+ function _buildViewToggleEl(sessionId) {
1044
1464
  const toggle = document.createElement('div');
1045
1465
  toggle.className = 'stream-view-toggle';
1046
1466
  toggle.setAttribute('role', 'group');
@@ -1064,22 +1484,48 @@ function createViewToggle(sessionId) {
1064
1484
  convBtn.setAttribute('aria-label', 'Show structured conversation');
1065
1485
  convBtn.innerHTML = '<span class="stream-view-icon" aria-hidden="true">C</span><span class="stream-view-label">Conversation</span>';
1066
1486
 
1067
- termBtn.onclick = () => {
1068
- setSessionView(sessionId, 'terminal');
1069
- };
1070
-
1071
- convBtn.onclick = () => {
1072
- setSessionView(sessionId, 'conversation');
1073
- };
1487
+ termBtn.onclick = () => { setSessionView(sessionId, 'terminal'); };
1488
+ convBtn.onclick = () => { setSessionView(sessionId, 'conversation'); };
1074
1489
 
1075
1490
  toggle.appendChild(termBtn);
1076
1491
  toggle.appendChild(convBtn);
1077
- controls.appendChild(toggle);
1078
- controls.appendChild(_buildExportMenu(sessionId));
1079
1492
  _setViewButtonState([termBtn, convBtn], _streamState.activeView.get(sessionId) || getPreferredSessionView());
1493
+ return toggle;
1494
+ }
1495
+
1496
+ function createViewToggle(sessionId) {
1497
+ const controls = document.createElement('div');
1498
+ controls.className = 'stream-view-controls';
1499
+ if (_sessionHasConversationView(sessionId)) controls.appendChild(_buildViewToggleEl(sessionId));
1500
+ controls.appendChild(_buildExportMenu(sessionId));
1080
1501
  return controls;
1081
1502
  }
1082
1503
 
1504
+ // Reconcile the Term/Conv toggle's presence after the agent type resolves. A
1505
+ // freshly-restored session can be classified as 'shell' before its agentType is
1506
+ // known, so createViewToggle skips the toggle and nothing rebuilds the toolbar —
1507
+ // leaving a Claude/Codex session permanently without Term/Conv. Add the toggle in
1508
+ // place (before the ⋯ menu) once the session is known to have a conversation
1509
+ // view; drop a stale one otherwise. Located via the toolbar's data-session-id so
1510
+ // it works whether the toolbar is in its pane or merged onto the tab strip.
1511
+ function syncSessionViewToggle(sessionId) {
1512
+ if (typeof document === 'undefined' || !sessionId) return;
1513
+ const esc = (typeof window !== 'undefined' && window.CSS && CSS.escape) ? CSS.escape(sessionId) : sessionId;
1514
+ const toolbar = document.querySelector(`.session-toolbar[data-session-id="${esc}"]`);
1515
+ const controls = toolbar ? toolbar.querySelector('.stream-view-controls') : null;
1516
+ if (!controls) return;
1517
+ const wantToggle = _sessionHasConversationView(sessionId);
1518
+ const existing = controls.querySelector('.stream-view-toggle');
1519
+ if (wantToggle && !existing) {
1520
+ const exportMenu = controls.querySelector('.stream-export-menu');
1521
+ controls.insertBefore(_buildViewToggleEl(sessionId), exportMenu || null);
1522
+ _syncViewToggle(sessionId);
1523
+ } else if (!wantToggle && existing) {
1524
+ existing.remove();
1525
+ }
1526
+ }
1527
+ if (typeof window !== 'undefined') window.syncSessionViewToggle = syncSessionViewToggle;
1528
+
1083
1529
  function _setViewButtonState(buttons, view) {
1084
1530
  buttons.forEach((btn) => {
1085
1531
  const active = btn.dataset.view === view;
@@ -1090,10 +1536,13 @@ function _setViewButtonState(buttons, view) {
1090
1536
 
1091
1537
  function _syncViewToggle(sessionId) {
1092
1538
  const view = _streamState.activeView.get(sessionId) || 'terminal';
1093
- const root = document.querySelector(`.term-container[data-session-id="${CSS.escape(sessionId)}"]`);
1094
- const buttons = root
1095
- ? root.querySelectorAll('.stream-view-btn[data-view]')
1096
- : document.querySelectorAll(`.stream-view-btn[data-view][data-session-id="${CSS.escape(sessionId)}"]`);
1539
+ // Select by data-session-id globally rather than scoping to the session's
1540
+ // .term-container: in the merged-row layout the toggle is relocated out of
1541
+ // the container into #tabbar-session-controls, so a container-scoped query
1542
+ // finds zero buttons and the active highlight never updates. Each button
1543
+ // carries data-session-id, so this matches it wherever it lives (container,
1544
+ // merged tab-strip slot, or a split pane).
1545
+ const buttons = document.querySelectorAll(`.stream-view-btn[data-view][data-session-id="${CSS.escape(sessionId)}"]`);
1097
1546
  _setViewButtonState(buttons, view);
1098
1547
  }
1099
1548
 
@@ -1105,8 +1554,18 @@ function _latestAnswerUiEnabled() {
1105
1554
  window._ctmIsLatestAnswerUiEnabled();
1106
1555
  }
1107
1556
 
1557
+ function _finalSummaryUiEnabled() {
1558
+ return typeof window !== 'undefined' &&
1559
+ typeof window._ctmIsFinalSummaryUiEnabled === 'function' &&
1560
+ window._ctmIsFinalSummaryUiEnabled();
1561
+ }
1562
+
1563
+ function _latestAnswerHydrationEnabled() {
1564
+ return _latestAnswerUiEnabled() || _finalSummaryUiEnabled();
1565
+ }
1566
+
1108
1567
  function _hydrateLatestAnswerFromConversationMessages(sessionId, messages, source, opts) {
1109
- if (!_latestAnswerUiEnabled()) return false;
1568
+ if (!_latestAnswerHydrationEnabled()) return false;
1110
1569
  if (typeof window === 'undefined' || typeof window._ctmHydrateLatestAnswerFromMessages !== 'function') return false;
1111
1570
  try {
1112
1571
  return window._ctmHydrateLatestAnswerFromMessages(sessionId, messages, {
@@ -1120,7 +1579,7 @@ function _hydrateLatestAnswerFromConversationMessages(sessionId, messages, sourc
1120
1579
  }
1121
1580
 
1122
1581
  async function _primeLatestAnswerForTerminal(sessionId, opts) {
1123
- if (!_latestAnswerUiEnabled()) return null;
1582
+ if (!_latestAnswerHydrationEnabled()) return null;
1124
1583
  opts = opts || {};
1125
1584
  const now = Date.now();
1126
1585
  const cached = _latestAnswerTerminalPrime.get(sessionId);
@@ -1178,9 +1637,36 @@ function _buildExportMenu(sessionId) {
1178
1637
  menu.className = 'stream-export-menu-items';
1179
1638
  menu.setAttribute('role', 'menu');
1180
1639
 
1640
+ // The dropdown is position:fixed (viewport-relative) so it escapes the
1641
+ // #tabbar { overflow:hidden } clip when the controls are merged onto the tab
1642
+ // strip. Anchor it under the trigger, right-aligned, clamped to the viewport.
1643
+ const positionMenu = () => {
1644
+ const rect = trigger.getBoundingClientRect();
1645
+ const margin = 8;
1646
+ menu.style.visibility = 'hidden';
1647
+ menu.style.top = '0px';
1648
+ menu.style.left = '0px';
1649
+ const w = menu.offsetWidth || 168;
1650
+ const h = menu.offsetHeight || 0;
1651
+ const left = Math.max(margin, Math.min(rect.right - w, window.innerWidth - w - margin));
1652
+ let top = rect.bottom + 4;
1653
+ if (top + h > window.innerHeight - margin) {
1654
+ top = Math.max(margin, window.innerHeight - h - margin);
1655
+ }
1656
+ menu.style.left = left + 'px';
1657
+ menu.style.top = top + 'px';
1658
+ menu.style.visibility = '';
1659
+ };
1660
+
1661
+ let reposition = null;
1181
1662
  const closeMenu = () => {
1182
1663
  container.classList.remove('open');
1183
1664
  trigger.setAttribute('aria-expanded', 'false');
1665
+ if (reposition) {
1666
+ window.removeEventListener('scroll', reposition, true);
1667
+ window.removeEventListener('resize', reposition);
1668
+ reposition = null;
1669
+ }
1184
1670
  };
1185
1671
 
1186
1672
  const addSection = (label) => {
@@ -1224,6 +1710,16 @@ function _buildExportMenu(sessionId) {
1224
1710
  }
1225
1711
  };
1226
1712
 
1713
+ addSection('Session');
1714
+ addItem('📋 Copy command', () => {
1715
+ const sess = (typeof state !== 'undefined' && state.sessions) ? state.sessions.get(sessionId) : null;
1716
+ const cwd = (sess && ((sess.meta && sess.meta.cwd) || sess.cwd)) || '';
1717
+ if (typeof window.copySessionCommand === 'function') window.copySessionCommand(sessionId, cwd);
1718
+ });
1719
+ addItem('⬚ Split right', () => { if (typeof window.splitFocusedPane === 'function') window.splitFocusedPane('h'); });
1720
+ addItem('⬚ Split down', () => { if (typeof window.splitFocusedPane === 'function') window.splitFocusedPane('v'); });
1721
+ addDivider();
1722
+
1227
1723
  addSection('Review');
1228
1724
  addItem('📝 Transcript', () => {
1229
1725
  const caps = (typeof window !== 'undefined' && typeof window._ctmAgentCapsForSession === 'function')
@@ -1291,6 +1787,14 @@ function _buildExportMenu(sessionId) {
1291
1787
  const open = !container.classList.contains('open');
1292
1788
  container.classList.toggle('open', open);
1293
1789
  trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
1790
+ if (open) {
1791
+ positionMenu();
1792
+ reposition = () => { if (container.classList.contains('open')) positionMenu(); };
1793
+ window.addEventListener('scroll', reposition, true);
1794
+ window.addEventListener('resize', reposition);
1795
+ } else {
1796
+ closeMenu();
1797
+ }
1294
1798
  };
1295
1799
  document.addEventListener('click', closeMenu);
1296
1800
 
@@ -1328,7 +1832,12 @@ function setSessionView(sessionId, view, opts) {
1328
1832
  if (xtermEl) xtermEl.style.display = '';
1329
1833
  if (convView) convView.style.display = 'none';
1330
1834
  if (typeof _renderCodexFinalPanel === 'function') _renderCodexFinalPanel(sessionId);
1331
- if (_latestAnswerUiEnabled()) _primeLatestAnswerForTerminal(sessionId);
1835
+ if (_latestAnswerHydrationEnabled()) _primeLatestAnswerForTerminal(sessionId);
1836
+ if (!opts.skipTerminalRestore &&
1837
+ typeof window !== 'undefined' &&
1838
+ typeof window._ctmEnsureTerminalRestoreForVisibleView === 'function') {
1839
+ window._ctmEnsureTerminalRestoreForVisibleView(sessionId, { reason: 'switch-to-terminal-view' });
1840
+ }
1332
1841
  if (window._ws) unsubscribeFromStream(window._ws, sessionId);
1333
1842
  }
1334
1843
  return view;
@@ -1353,6 +1862,8 @@ function _applyStreamEvent(sessionId, container, msg) {
1353
1862
  const refreshLatestPromptStatus = () => {
1354
1863
  if (_isPromptTurnContainer(container)) _refreshLatestPromptTurnStatus(sessionId, container);
1355
1864
  };
1865
+ const scrollAnchor = _captureConversationScrollAnchor(container);
1866
+ const restoreScrollAnchor = () => _restoreConversationScrollAnchor(container, scrollAnchor);
1356
1867
 
1357
1868
  // Explicit update (server signaled _update=true): replace the row with
1358
1869
  // this parentUuid.
@@ -1364,11 +1875,14 @@ function _applyStreamEvent(sessionId, container, msg) {
1364
1875
  : renderConversationEvent(msg);
1365
1876
  if (newEl) {
1366
1877
  _assignConversationParentUuid(newEl, parentUuid);
1878
+ _linkConversationDocumentReferences(container, newEl);
1367
1879
  _replaceConversationParentEvent(existing, newEl);
1880
+ _markConversationEventSeen(sessionId, msg);
1368
1881
  } else {
1369
1882
  _replaceConversationParentEvent(existing, null); // Event became empty after update
1370
1883
  }
1371
1884
  refreshLatestPromptStatus();
1885
+ restoreScrollAnchor();
1372
1886
  return;
1373
1887
  }
1374
1888
  }
@@ -1385,9 +1899,12 @@ function _applyStreamEvent(sessionId, container, msg) {
1385
1899
  : renderConversationEvent(msg);
1386
1900
  if (newEl) {
1387
1901
  _assignConversationParentUuid(newEl, parentUuid);
1902
+ _linkConversationDocumentReferences(container, newEl);
1388
1903
  _replaceConversationParentEvent(existing, newEl);
1904
+ _markConversationEventSeen(sessionId, msg);
1389
1905
  }
1390
1906
  refreshLatestPromptStatus();
1907
+ restoreScrollAnchor();
1391
1908
  return;
1392
1909
  }
1393
1910
  }
@@ -1399,14 +1916,37 @@ function _applyStreamEvent(sessionId, container, msg) {
1399
1916
  : renderConversationEvent(msg);
1400
1917
  if (newEl) {
1401
1918
  _assignConversationParentUuid(newEl, parentUuid);
1919
+ _linkConversationDocumentReferences(container, newEl);
1402
1920
  _replaceConversationParentEvent(existing, newEl);
1403
1921
  seen.add(parentUuid);
1922
+ _markConversationEventSeen(sessionId, msg);
1404
1923
  }
1405
1924
  refreshLatestPromptStatus();
1925
+ restoreScrollAnchor();
1406
1926
  return;
1407
1927
  }
1408
1928
  }
1409
1929
 
1930
+ if (_conversationEventAlreadySeen(sessionId, msg)) {
1931
+ if (parentUuid) seen.add(parentUuid);
1932
+ refreshLatestPromptStatus();
1933
+ restoreScrollAnchor();
1934
+ return;
1935
+ }
1936
+
1937
+ const duplicatePromptTurn = _findDuplicateConversationPromptTurn(container, msg);
1938
+ if (duplicatePromptTurn) {
1939
+ _conversationStampPromptTurn(duplicatePromptTurn, msg);
1940
+ if (parentUuid) {
1941
+ if (!duplicatePromptTurn.dataset?.parentUuid) _assignConversationParentUuid(duplicatePromptTurn, parentUuid);
1942
+ seen.add(parentUuid);
1943
+ }
1944
+ _markConversationEventSeen(sessionId, msg);
1945
+ refreshLatestPromptStatus();
1946
+ restoreScrollAnchor();
1947
+ return;
1948
+ }
1949
+
1410
1950
  const el = _isPromptTurnContainer(container)
1411
1951
  ? _appendConversationEventToTurns(container, msg, { expandPrompt: false, collapseExisting: true, expandSetup: true })
1412
1952
  : renderConversationEvent(msg);
@@ -1415,6 +1955,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1415
1955
  _assignConversationParentUuid(el, parentUuid);
1416
1956
  if (seen) seen.add(parentUuid);
1417
1957
  }
1958
+ _markConversationEventSeen(sessionId, msg);
1418
1959
  // If this is a tool-only thought-group AND the last rendered child is also
1419
1960
  // a tool-only thought-group, fold this one's single step into the existing
1420
1961
  // group instead of appending a new wrapper. Matches Review's grouping.
@@ -1425,6 +1966,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1425
1966
  } else {
1426
1967
  container.appendChild(el);
1427
1968
  }
1969
+ _linkConversationDocumentReferences(container, container);
1428
1970
  // Phase 4 reconciliation: respect the global "Hide tool calls" toggle
1429
1971
  // for newly-streamed tool-only rows so the user's preference applies
1430
1972
  // immediately to live updates, not just to the next full re-render.
@@ -1432,9 +1974,9 @@ function _applyStreamEvent(sessionId, container, msg) {
1432
1974
  const cb = document.getElementById('hide-tool-msgs');
1433
1975
  if (cb && cb.checked) el.style.display = 'none';
1434
1976
  }
1977
+ _pruneConversationTurns(container);
1435
1978
  refreshLatestPromptStatus();
1436
- const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 100;
1437
- if (atBottom) container.scrollTop = container.scrollHeight;
1979
+ restoreScrollAnchor();
1438
1980
  }
1439
1981
 
1440
1982
  // Prime the conversation view with the full cached history from
@@ -1445,12 +1987,10 @@ function _applyStreamEvent(sessionId, container, msg) {
1445
1987
  // the same promise. If priming has already completed, returns immediately.
1446
1988
  // Call this on stream-init; stream-events arriving during the HTTP fetch
1447
1989
  // are buffered in _streamState._pendingEvents and flushed after prime.
1448
- // Pagination support for chat-style conversation loading. Matches the
1449
- // upstream claude-code-history-viewer contract (src-tauri's
1450
- // load_session_messages_paginated): offset=0 returns the most recent N
1451
- // messages; each successive page walks backward in history. See
1452
- // gap-reports/viewer-2026-04-23.md item D.
1453
- const CONVERSATION_PAGE_SIZE = 200;
1990
+ // Pagination support for chat-style conversation loading. The server returns
1991
+ // complete semantic turns for this view so pages never start inside a long
1992
+ // tool-heavy turn without the owning prompt.
1993
+ const CONVERSATION_TURN_PAGE_SIZE = 20;
1454
1994
 
1455
1995
  function _conversationViewHasRenderableContent(convView) {
1456
1996
  if (!convView || !convView.children || convView.children.length === 0) return false;
@@ -1489,24 +2029,54 @@ function _primeStillCurrent(sessionId, token, convView) {
1489
2029
  }
1490
2030
 
1491
2031
  function _cleanSystemXml(t) {
1492
- return t
2032
+ return String(t || '')
1493
2033
  .replace(/<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[^>]*>[\s\S]*?<\/\1>/gi, '')
1494
2034
  .replace(/<\/?[a-z][a-z0-9]*(?:-[a-z0-9]+)+[^>]*>/gi, '')
1495
2035
  .trim();
1496
2036
  }
1497
2037
 
2038
+ function _messageTextForConversationEvent(message) {
2039
+ if (!message || typeof message !== 'object') return '';
2040
+ const direct = message.text ?? message.content ?? message.message ?? '';
2041
+ if (typeof direct === 'string') return direct;
2042
+ if (Array.isArray(direct)) {
2043
+ return direct.map(part => {
2044
+ if (typeof part === 'string') return part;
2045
+ if (!part || typeof part !== 'object') return '';
2046
+ return part.text || part.content || part.message || '';
2047
+ }).filter(Boolean).join('\n');
2048
+ }
2049
+ if (direct && typeof direct === 'object') return direct.text || direct.content || direct.message || '';
2050
+ return '';
2051
+ }
2052
+
1498
2053
  function _messagesToEvents(messages) {
1499
2054
  return messages
1500
- .filter(m => m.role === 'user' || m.role === 'assistant')
1501
- .map(m => ({
1502
- type: m.role,
1503
- data: {
1504
- text: m.role === 'user' ? _cleanSystemXml(m.text || '') : (m.text || ''),
2055
+ .filter(m => m.role === 'user' || m.role === 'assistant' || m.role === 'system')
2056
+ .map(m => {
2057
+ const rawText = _messageTextForConversationEvent(m);
2058
+ const cleanedUserText = m.role === 'user' ? _cleanSystemXml(rawText) : rawText;
2059
+ const isSystemOnlyUser = m.role === 'user' && rawText && !cleanedUserText;
2060
+ const type = (m.role === 'system' || isSystemOnlyUser) ? 'summary' : m.role;
2061
+ const data = {
2062
+ text: type === 'summary' ? rawText : cleanedUserText,
1505
2063
  model: m.model || '',
1506
2064
  parentUuid: m.parentUuid,
1507
- },
1508
- timestamp: m.timestamp,
1509
- }));
2065
+ };
2066
+ if (m.image_refs) data.image_refs = m.image_refs;
2067
+ if (m.imageRefs) data.imageRefs = m.imageRefs;
2068
+ if (m.attachments) data.attachments = m.attachments;
2069
+ if (m.images) data.images = m.images;
2070
+ if (m.contentBlocks) data.contentBlocks = m.contentBlocks;
2071
+ // Structured-capture rows (tool cards, reasoning, dividers) and subagent
2072
+ // tags need their metadata on the event or MR.messageMeta can't see it.
2073
+ if (m.metadata) data.metadata = m.metadata;
2074
+ return {
2075
+ type,
2076
+ data,
2077
+ timestamp: m.timestamp,
2078
+ };
2079
+ });
1510
2080
  }
1511
2081
 
1512
2082
  function _renderConversationState(container, text, kind = 'empty') {
@@ -1564,13 +2134,15 @@ function _removeConversationState(container) {
1564
2134
 
1565
2135
  function _normalizeConversationPage(body) {
1566
2136
  if (Array.isArray(body)) {
1567
- return { messages: body, total: body.length, has_more: false, next_offset: body.length };
2137
+ return { messages: body, total: body.length, has_more: false, next_offset: body.length, page_kind: 'messages', turn_count: 0 };
1568
2138
  }
1569
2139
  return {
1570
2140
  messages: Array.isArray(body?.messages) ? body.messages : [],
1571
2141
  total: typeof body?.total === 'number' ? body.total : 0,
1572
2142
  has_more: !!body?.has_more,
1573
2143
  next_offset: typeof body?.next_offset === 'number' ? body.next_offset : 0,
2144
+ page_kind: body?.page_kind || 'messages',
2145
+ turn_count: typeof body?.turn_count === 'number' ? body.turn_count : 0,
1574
2146
  partial: !!body?.partial,
1575
2147
  };
1576
2148
  }
@@ -1634,8 +2206,8 @@ function _conversationLookupCandidates(sessionId) {
1634
2206
  }
1635
2207
 
1636
2208
  /**
1637
- * Fetch one chat-style page of messages. offset=0 yields the most recent
1638
- * CONVERSATION_PAGE_SIZE messages. Backend returns either an array
2209
+ * Fetch one chat-style page of turns. offset=0 yields the most recent
2210
+ * CONVERSATION_TURN_PAGE_SIZE turns. Backend returns either an array
1639
2211
  * (legacy/unpaginated response for other callers) or a structured page:
1640
2212
  * { messages, total, has_more, next_offset, partial? }
1641
2213
  */
@@ -1645,7 +2217,7 @@ async function _fetchConversationPage(sessionId, offset, opts) {
1645
2217
  let firstEmptyPage = null;
1646
2218
  for (const candidate of candidates) {
1647
2219
  let url = `/api/session/messages?id=${encodeURIComponent(candidate.id)}`
1648
- + `&offset=${offset}&limit=${CONVERSATION_PAGE_SIZE}`;
2220
+ + `&offset=${offset}&limit=${CONVERSATION_TURN_PAGE_SIZE}&mode=turns`;
1649
2221
  if (candidate.projectEntry) url += `&project=${encodeURIComponent(candidate.projectEntry)}`;
1650
2222
  if (opts.fresh && offset === 0) url += '&nocache=1';
1651
2223
  const resp = await fetch(url, { cache: 'no-store' });
@@ -1664,18 +2236,56 @@ function _ensureLoadOlderBar(convView, sessionId) {
1664
2236
  bar.className = 'conversation-load-older';
1665
2237
  bar.type = 'button';
1666
2238
  bar.style.cssText = 'display:none;margin:0 auto 8px;padding:6px 14px;border:1px solid var(--conversation-load-border, var(--border, #333));background:var(--conversation-load-bg, var(--surface-2, #1a1a2e));color:var(--conversation-load-fg, var(--fg-dim, #999));border-radius:6px;font-size:11px;cursor:pointer;';
1667
- bar.textContent = `Load ${CONVERSATION_PAGE_SIZE} older messages`;
2239
+ bar.textContent = `Load ${CONVERSATION_TURN_PAGE_SIZE} older turns`;
1668
2240
  bar.onclick = () => _loadOlder(sessionId, convView);
1669
2241
  convView.insertBefore(bar, convView.firstChild);
2242
+ // Auto-load when the bar scrolls into view (button stays as the fallback
2243
+ // affordance). bar.disabled already provides re-entrancy guarding.
2244
+ if (typeof IntersectionObserver === 'function') {
2245
+ const observer = new IntersectionObserver((entries) => {
2246
+ for (const entry of entries) {
2247
+ if (!entry.isIntersecting) continue;
2248
+ if (convView.dataset.hasMore !== '1' || bar.disabled || bar.style.display === 'none') continue;
2249
+ _loadOlder(sessionId, convView);
2250
+ }
2251
+ }, { root: convView, threshold: 0 });
2252
+ observer.observe(bar);
2253
+ }
1670
2254
  return bar;
1671
2255
  }
1672
2256
 
2257
+ // Soft DOM cap: a very long live session accumulates turns without bound.
2258
+ // Above the cap, prune the OLDEST rendered turns and re-arm the load-older
2259
+ // bar at the recomputed offset — scroll-up refetches them (turn pages are
2260
+ // O(limit) on the server), which replaces height-placeholder bookkeeping.
2261
+ // Only prunes while the user is following the bottom, so it can never yank
2262
+ // the viewport from under someone reading history.
2263
+ const MAX_RENDERED_TURNS = 400;
2264
+ function _pruneConversationTurns(convView) {
2265
+ if (!convView || typeof convView.querySelectorAll !== 'function') return;
2266
+ if (!_isPromptTurnContainer(convView)) return;
2267
+ if (!_conversationScrollNearBottom(convView)) return;
2268
+ const turns = convView.querySelectorAll(':scope > .prompt-turn:not(.setup-turn)');
2269
+ const excess = turns.length - MAX_RENDERED_TURNS;
2270
+ if (excess <= 0) return;
2271
+ for (let i = 0; i < excess; i++) turns[i].remove();
2272
+ const nextOffset = Math.max(0, (parseInt(convView.dataset.nextOffset || '0', 10) || 0) - excess);
2273
+ convView.dataset.nextOffset = String(nextOffset);
2274
+ convView.dataset.hasMore = '1';
2275
+ const bar = convView.querySelector(':scope > .conversation-load-older');
2276
+ if (bar) {
2277
+ bar.textContent = 'Load older turns';
2278
+ bar.style.display = 'block';
2279
+ }
2280
+ }
2281
+
1673
2282
  function _updateLoadOlderBar(convView, page) {
1674
2283
  const bar = convView.querySelector(':scope > .conversation-load-older');
1675
2284
  if (!bar) return;
1676
2285
  if (page && page.has_more) {
1677
2286
  const remaining = Math.max(0, page.total - page.next_offset);
1678
- bar.textContent = `Load older messages (${remaining} remaining)`;
2287
+ const unit = page.page_kind === 'turns' ? 'turns' : 'messages';
2288
+ bar.textContent = `Load older ${unit} (${remaining} remaining)`;
1679
2289
  bar.style.display = 'block';
1680
2290
  } else {
1681
2291
  bar.style.display = 'none';
@@ -1711,18 +2321,22 @@ async function _loadOlder(sessionId, convView) {
1711
2321
  while (pageHost.firstChild) frag.appendChild(pageHost.firstChild);
1712
2322
  // Prepend below the bar so the bar stays at the very top.
1713
2323
  convView.insertBefore(frag, bar.nextSibling);
2324
+ _linkConversationDocumentReferences(convView, convView);
1714
2325
  const newScrollHeight = convView.scrollHeight;
1715
2326
  convView.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight);
1716
2327
  // Merge new parentUuids into the dedup set so live events for primed
1717
2328
  // turns still find and replace their older-page counterparts.
1718
2329
  const seen = _streamState._parentUuidSeen.get(sessionId) || new Set();
1719
- for (const e of events) if (e.data?.parentUuid) seen.add(e.data.parentUuid);
2330
+ for (const e of events) {
2331
+ if (e.data?.parentUuid) seen.add(e.data.parentUuid);
2332
+ _markConversationEventSeen(sessionId, e);
2333
+ }
1720
2334
  _streamState._parentUuidSeen.set(sessionId, seen);
1721
2335
  _updateLoadOlderBar(convView, page);
1722
2336
  _refreshLatestPromptTurnStatus(sessionId, convView);
1723
2337
  } catch (e) {
1724
2338
  console.error('[stream-view] load older failed:', e && e.message);
1725
- bar.textContent = `Load ${CONVERSATION_PAGE_SIZE} older messages`;
2339
+ bar.textContent = `Load ${CONVERSATION_TURN_PAGE_SIZE} older turns`;
1726
2340
  } finally {
1727
2341
  bar.disabled = false;
1728
2342
  }
@@ -1742,6 +2356,7 @@ async function _primeConversationView(sessionId, convView, opts) {
1742
2356
  const token = _nextPrimeToken(sessionId);
1743
2357
  _streamState._primed.delete(sessionId);
1744
2358
  _streamState._primedTarget.delete(sessionId);
2359
+ _streamState._messageFingerprintSeen.delete(sessionId);
1745
2360
  _setConversationBusy(convView, true);
1746
2361
  _renderConversationLoading(convView);
1747
2362
 
@@ -1836,6 +2451,7 @@ function _resetPrimingState(sessionId) {
1836
2451
  _streamState._primingTarget.delete(sessionId);
1837
2452
  _streamState._pendingEvents.delete(sessionId);
1838
2453
  _streamState._parentUuidSeen.delete(sessionId);
2454
+ _streamState._messageFingerprintSeen.delete(sessionId);
1839
2455
  }
1840
2456
 
1841
2457
  // --- Tooltip hover binding ---
@@ -1963,6 +2579,8 @@ if (typeof module !== 'undefined' && module.exports) {
1963
2579
  renderConversationEvent,
1964
2580
  populateConversationView,
1965
2581
  _mergeConsecutiveToolGroups,
2582
+ _captureConversationScrollAnchor,
2583
+ _restoreConversationScrollAnchor,
1966
2584
  _streamState,
1967
2585
  getPreferredSessionView,
1968
2586
  setPreferredSessionView,