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
@@ -8,11 +8,16 @@ window.WalleSession = (function() {
8
8
 
9
9
  var TRANSCRIPT_ATTACH_DEBOUNCE_MS = 120;
10
10
  var TRANSCRIPT_LIVE_ECHO_SKIP_MS = 1500;
11
+ var HISTORY_RETRY_AFTER_MS = 2500;
11
12
  var COMPOSER_HEIGHT_STORAGE_KEY = 'ctm.walle.composerHeightPx';
12
13
  var COMPOSER_HEIGHT_STEP_PX = 16;
13
14
  var COMPOSER_HEIGHT_DEFAULT_PX = 44;
14
15
  var COMPOSER_HEIGHT_MIN_PX = 44;
15
16
  var COMPOSER_HEIGHT_MAX_PX = 420;
17
+ var MESSAGE_RENDER_SYNC_LIMIT = 60;
18
+ var MESSAGE_RENDER_BATCH_SIZE = 24;
19
+ var MESSAGE_RENDER_FRAME_BUDGET_MS = 8;
20
+ var _cancelConfirmationOpen = false;
16
21
 
17
22
  function loadSavedComposerHeight() {
18
23
  try {
@@ -40,7 +45,7 @@ window.WalleSession = (function() {
40
45
  // ---------- state helper ----------
41
46
  function initialModelFromSession(s) {
42
47
  var meta = (s && s.meta) || {};
43
- var model = meta.model_id || meta.model || '';
48
+ var model = normalizeSessionModelId(meta.model_id || meta.model || '');
44
49
  return {
45
50
  model: model,
46
51
  registryId: meta.model_registry_id || meta.modelRegistryId || '',
@@ -73,10 +78,31 @@ window.WalleSession = (function() {
73
78
  inputHistory: [],
74
79
  inputHistoryIdx: -1,
75
80
  inputDraft: '',
81
+ composerDraft: {
82
+ value: '',
83
+ selectionStart: 0,
84
+ selectionEnd: 0,
85
+ focused: false,
86
+ updatedAt: 0
87
+ },
76
88
  inputAttachments: [],
89
+ queueState: null,
90
+ workState: null,
91
+ workDrawerOpen: false,
92
+ isQueueingMessage: false,
77
93
  composerPreviewOpen: false,
78
94
  isPreparingLocation: false,
79
- composerHeightPx: loadSavedComposerHeight()
95
+ historyStatus: s.needsAttach ? 'loading' : (s._walleHistoryLoaded ? 'loaded' : 'idle'),
96
+ historyRequestedAt: s._walleHistoryRequestedAt || 0,
97
+ historyLoadedAt: s._walleHistoryLoadedAt || 0,
98
+ historyLoadReason: '',
99
+ composerHeightPx: loadSavedComposerHeight(),
100
+ branches: {},
101
+ branchActive: {},
102
+ editingMessageIndex: -1,
103
+ _branchHistoryLoaded: false,
104
+ _branchHistoryAuthoritative: false,
105
+ _branchActiveHistory: []
80
106
  };
81
107
  } else if (!s.walleState._modelHydrated) {
82
108
  s.walleState.selectedModel = s.walleState.selectedModel || initialModel.model;
@@ -94,9 +120,33 @@ window.WalleSession = (function() {
94
120
  if (!Array.isArray(s.walleState.inputHistory)) s.walleState.inputHistory = [];
95
121
  if (typeof s.walleState.inputHistoryIdx !== 'number') s.walleState.inputHistoryIdx = -1;
96
122
  if (typeof s.walleState.inputDraft !== 'string') s.walleState.inputDraft = '';
123
+ if (!s.walleState.composerDraft || typeof s.walleState.composerDraft !== 'object') {
124
+ s.walleState.composerDraft = { value: '', selectionStart: 0, selectionEnd: 0, focused: false, updatedAt: 0 };
125
+ }
126
+ if (typeof s.walleState.composerDraft.value !== 'string') s.walleState.composerDraft.value = '';
127
+ if (typeof s.walleState.composerDraft.selectionStart !== 'number') s.walleState.composerDraft.selectionStart = 0;
128
+ if (typeof s.walleState.composerDraft.selectionEnd !== 'number') s.walleState.composerDraft.selectionEnd = s.walleState.composerDraft.selectionStart;
129
+ if (typeof s.walleState.composerDraft.focused !== 'boolean') s.walleState.composerDraft.focused = false;
130
+ if (typeof s.walleState.composerDraft.updatedAt !== 'number') s.walleState.composerDraft.updatedAt = 0;
97
131
  if (!Array.isArray(s.walleState.inputAttachments)) s.walleState.inputAttachments = [];
132
+ if (typeof s.walleState.isQueueingMessage !== 'boolean') s.walleState.isQueueingMessage = false;
133
+ if (s.walleState.queueState && typeof s.walleState.queueState !== 'object') s.walleState.queueState = null;
134
+ if (s.walleState.workState && typeof s.walleState.workState !== 'object') s.walleState.workState = null;
135
+ if (typeof s.walleState.workDrawerOpen !== 'boolean') s.walleState.workDrawerOpen = false;
136
+ if (!s.walleState.branches || typeof s.walleState.branches !== 'object') s.walleState.branches = {};
137
+ if (!s.walleState.branchActive || typeof s.walleState.branchActive !== 'object') s.walleState.branchActive = {};
138
+ if (typeof s.walleState.editingMessageIndex !== 'number') s.walleState.editingMessageIndex = -1;
139
+ if (!Array.isArray(s.walleState._branchActiveHistory)) s.walleState._branchActiveHistory = [];
140
+ if (typeof s.walleState._branchHistoryLoaded !== 'boolean') s.walleState._branchHistoryLoaded = false;
141
+ if (typeof s.walleState._branchHistoryAuthoritative !== 'boolean') s.walleState._branchHistoryAuthoritative = false;
98
142
  if (typeof s.walleState.composerPreviewOpen !== 'boolean') s.walleState.composerPreviewOpen = false;
99
143
  if (typeof s.walleState.isPreparingLocation !== 'boolean') s.walleState.isPreparingLocation = false;
144
+ if (!s.walleState.historyStatus) {
145
+ s.walleState.historyStatus = s.needsAttach ? 'loading' : (s._walleHistoryLoaded ? 'loaded' : 'idle');
146
+ }
147
+ if (typeof s.walleState.historyRequestedAt !== 'number') s.walleState.historyRequestedAt = s._walleHistoryRequestedAt || 0;
148
+ if (typeof s.walleState.historyLoadedAt !== 'number') s.walleState.historyLoadedAt = s._walleHistoryLoadedAt || 0;
149
+ if (typeof s.walleState.historyLoadReason !== 'string') s.walleState.historyLoadReason = '';
100
150
  if (typeof s.walleState.selectedModelRegistryId !== 'string') s.walleState.selectedModelRegistryId = '';
101
151
  if (typeof s.walleState.selectedModelProviderType !== 'string') s.walleState.selectedModelProviderType = '';
102
152
  if ((s.meta && (s.meta.model_pinned === true || s.meta.modelPinned === true)) && s.walleState.selectedModel) {
@@ -108,6 +158,383 @@ window.WalleSession = (function() {
108
158
  return s.walleState;
109
159
  }
110
160
 
161
+ function walleBranchSessionId(id) {
162
+ var s = state.sessions.get(id);
163
+ var meta = (s && s.meta) || {};
164
+ return meta.chatSessionId
165
+ || meta.chat_session_id
166
+ || meta.agentSessionId
167
+ || meta.agent_session_id
168
+ || meta.walleChatSessionId
169
+ || meta.walle_chat_session_id
170
+ || ('walle-' + id);
171
+ }
172
+
173
+ function cloneJsonSafe(value, fallback) {
174
+ if (value == null) return fallback;
175
+ try {
176
+ return JSON.parse(JSON.stringify(value));
177
+ } catch (_) {
178
+ return fallback;
179
+ }
180
+ }
181
+
182
+ // Branch snapshots are persisted/transmitted as JSON, so they must not carry
183
+ // base64 image payloads: a single screenshot can be hundreds of KB, and a
184
+ // snapshot serializes the whole conversation across every branch version.
185
+ // Strip the heavy `data` field and keep only the lightweight reference fields
186
+ // — the renderer displays from `url` (renderAttachmentStrip) and the server
187
+ // rehydrates the base64 from disk by path/url/filename on resend
188
+ // (_hydrateWalleAttachment). The image lives in the images dir regardless.
189
+ var BRANCH_ATTACHMENT_REF_FIELDS = [
190
+ 'type', 'name', 'filename', 'label', 'url', 'path', 'file_path', 'id',
191
+ 'mediaType', 'mimeType', 'imageWidth', 'imageHeight',
192
+ 'originalWidth', 'originalHeight', 'resizedForProvider'
193
+ ];
194
+
195
+ function branchSafeAttachments(attachments) {
196
+ return (Array.isArray(attachments) ? attachments : []).map(function(att) {
197
+ if (!att || typeof att !== 'object') return att;
198
+ var out = {};
199
+ BRANCH_ATTACHMENT_REF_FIELDS.forEach(function(key) {
200
+ if (att[key] != null) out[key] = att[key];
201
+ });
202
+ return out;
203
+ });
204
+ }
205
+
206
+ function normalizeBranchMessage(msg) {
207
+ if (!msg || typeof msg !== 'object') return null;
208
+ var role = String(msg.role || '').trim();
209
+ if (!role) return null;
210
+ var content = msg.content != null ? msg.content : msg.text;
211
+ var out = {
212
+ role: role,
213
+ content: typeof content === 'string' ? content : String(content || ''),
214
+ timestamp: msg.timestamp || msg.created_at || msg.createdAt || Date.now()
215
+ };
216
+ var model = msg.model_id || msg.model || '';
217
+ if (model) out.model = model;
218
+ var latency = msg.latency_ms != null ? msg.latency_ms : msg.latencyMs;
219
+ if (latency != null) out.latency_ms = latency;
220
+ if (Array.isArray(msg.toolCalls)) out.toolCalls = cloneJsonSafe(msg.toolCalls, []);
221
+ else if (Array.isArray(msg.tool_calls)) out.toolCalls = cloneJsonSafe(msg.tool_calls, []);
222
+ if (Array.isArray(msg.attachments)) out.attachments = branchSafeAttachments(msg.attachments);
223
+ if (msg.metadata) out.metadata = cloneJsonSafe(msg.metadata, msg.metadata);
224
+ if (msg.liveActivity) out.liveActivity = true;
225
+ if (msg.agentLabel) out.agentLabel = msg.agentLabel;
226
+ return out;
227
+ }
228
+
229
+ function cloneBranchMessage(msg) {
230
+ return normalizeBranchMessage(msg) || {
231
+ role: '',
232
+ content: '',
233
+ timestamp: Date.now()
234
+ };
235
+ }
236
+
237
+ function normalizeBranchTail(tail) {
238
+ return (Array.isArray(tail) ? tail : [])
239
+ .map(normalizeBranchMessage)
240
+ .filter(function(msg) { return !!msg && !!msg.role; });
241
+ }
242
+
243
+ function branchMessageTimestampMs(msg) {
244
+ if (!msg) return 0;
245
+ var value = msg.timestamp || msg.created_at || msg.createdAt || 0;
246
+ if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
247
+ var parsed = Date.parse(String(value || ''));
248
+ return Number.isFinite(parsed) ? parsed : 0;
249
+ }
250
+
251
+ function branchAttachmentFingerprint(att) {
252
+ if (!att || typeof att !== 'object') return '';
253
+ return [
254
+ att.label || '',
255
+ att.filename || att.name || '',
256
+ att.url || '',
257
+ att.path || att.file_path || '',
258
+ att.id || ''
259
+ ].join('\u001f');
260
+ }
261
+
262
+ function branchMessageFingerprint(msg) {
263
+ if (!msg || !msg.role) return '';
264
+ var attachments = (Array.isArray(msg.attachments) ? msg.attachments : [])
265
+ .map(branchAttachmentFingerprint)
266
+ .filter(Boolean)
267
+ .sort()
268
+ .join('\u001e');
269
+ return [
270
+ String(msg.role || ''),
271
+ String(msg.content || ''),
272
+ String(branchMessageTimestampMs(msg) || ''),
273
+ attachments
274
+ ].join('\u001f');
275
+ }
276
+
277
+ function reconcileBranchHistoryWithDurable(branchHistory, durableHistory, snapshotSavedAtMs) {
278
+ var branch = normalizeBranchTail(branchHistory);
279
+ var durable = normalizeBranchTail(durableHistory);
280
+ if (!branch.length) return durable;
281
+ if (!durable.length) return branch;
282
+
283
+ var branchFingerprints = {};
284
+ branch.forEach(function(msg) {
285
+ var fp = branchMessageFingerprint(msg);
286
+ if (fp) branchFingerprints[fp] = true;
287
+ });
288
+
289
+ var lastDurableMatch = -1;
290
+ for (var b = branch.length - 1; b >= 0 && lastDurableMatch < 0; b--) {
291
+ var branchFp = branchMessageFingerprint(branch[b]);
292
+ if (!branchFp) continue;
293
+ for (var d = durable.length - 1; d >= 0; d--) {
294
+ if (branchMessageFingerprint(durable[d]) === branchFp) {
295
+ lastDurableMatch = d;
296
+ break;
297
+ }
298
+ }
299
+ }
300
+
301
+ var watermark = Number(snapshotSavedAtMs || 0) || 0;
302
+ var appendStart = lastDurableMatch >= 0 ? lastDurableMatch + 1 : 0;
303
+ var appended = [];
304
+ for (var i = appendStart; i < durable.length; i++) {
305
+ var candidate = durable[i];
306
+ var fp = branchMessageFingerprint(candidate);
307
+ if (fp && branchFingerprints[fp]) continue;
308
+ if (watermark > 0) {
309
+ var ts = branchMessageTimestampMs(candidate);
310
+ // New snapshots carry a save watermark. Do not resurrect old durable
311
+ // transcript tails that a branch edit/delete intentionally abandoned.
312
+ if (ts > 0 && ts <= watermark) continue;
313
+ } else if (lastDurableMatch < 0) {
314
+ continue;
315
+ }
316
+ appended.push(cloneBranchMessage(candidate));
317
+ }
318
+
319
+ return branch.map(cloneBranchMessage).concat(appended);
320
+ }
321
+
322
+ function normalizeBranchMap(branches) {
323
+ var out = {};
324
+ if (!branches || typeof branches !== 'object') return out;
325
+ Object.keys(branches).forEach(function(key) {
326
+ var idx = parseInt(key, 10);
327
+ if (!Number.isFinite(idx) || idx < 0) return;
328
+ var versions = Array.isArray(branches[key]) ? branches[key] : [];
329
+ var normalized = versions.map(normalizeBranchTail).filter(function(tail) { return tail.length > 0; });
330
+ if (normalized.length) out[String(idx)] = normalized;
331
+ });
332
+ return out;
333
+ }
334
+
335
+ function activeHistorySnapshot(ws) {
336
+ return ((ws && ws.messages) || []).map(cloneBranchMessage).filter(function(msg) { return !!msg.role; });
337
+ }
338
+
339
+ function branchKeys(ws) {
340
+ return Object.keys((ws && ws.branches) || {})
341
+ .map(function(key) { return parseInt(key, 10); })
342
+ .filter(function(idx) { return Number.isFinite(idx); })
343
+ .sort(function(a, b) { return a - b; });
344
+ }
345
+
346
+ function hasBranchSnapshot(ws) {
347
+ return branchKeys(ws).length > 0 || !!(ws && ws._branchHistoryAuthoritative);
348
+ }
349
+
350
+ function refreshActiveBranchSlots(ws) {
351
+ if (!ws || !ws.branches) return;
352
+ branchKeys(ws).forEach(function(splitIdx) {
353
+ if (splitIdx >= ws.messages.length) {
354
+ delete ws.branches[String(splitIdx)];
355
+ delete ws.branchActive[String(splitIdx)];
356
+ return;
357
+ }
358
+ var branches = ws.branches[String(splitIdx)];
359
+ if (!Array.isArray(branches) || !branches.length) return;
360
+ var active = Number(ws.branchActive[String(splitIdx)] || 0);
361
+ if (!Number.isFinite(active) || active < 0 || active >= branches.length) active = 0;
362
+ branches[active] = ws.messages.slice(splitIdx).map(cloneBranchMessage);
363
+ ws.branchActive[String(splitIdx)] = active;
364
+ });
365
+ }
366
+
367
+ function clearBranchesAfter(ws, splitIdx, inclusive) {
368
+ if (!ws || !ws.branches) return;
369
+ branchKeys(ws).forEach(function(key) {
370
+ if (inclusive ? key >= splitIdx : key > splitIdx) {
371
+ delete ws.branches[String(key)];
372
+ delete ws.branchActive[String(key)];
373
+ }
374
+ });
375
+ }
376
+
377
+ function markBranchHistoryAuthoritative(ws) {
378
+ if (!ws) return;
379
+ ws._branchHistoryAuthoritative = true;
380
+ ws._branchActiveHistory = activeHistorySnapshot(ws);
381
+ }
382
+
383
+ function applyBranchHistoryIfAuthoritative(ws, durableHistory) {
384
+ if (!ws || !ws._branchHistoryAuthoritative || !Array.isArray(ws._branchActiveHistory)) return false;
385
+ ws.messages = reconcileBranchHistoryWithDurable(
386
+ ws._branchActiveHistory,
387
+ durableHistory,
388
+ ws._branchSnapshotSavedAtMs
389
+ );
390
+ ws.messageCount = ws.messages.length;
391
+ return true;
392
+ }
393
+
394
+ function branchApiUrl(sessionId) {
395
+ return '/api/wall-e/chat/branches?session_id=' + encodeURIComponent(sessionId || 'default');
396
+ }
397
+
398
+ function saveWalleBranchSnapshot(id) {
399
+ var ws = getState(id);
400
+ if (!ws) return Promise.resolve(false);
401
+ refreshActiveBranchSlots(ws);
402
+ markBranchHistoryAuthoritative(ws);
403
+ var payload = {
404
+ session_id: walleBranchSessionId(id),
405
+ branches: ws.branches || {},
406
+ active: ws.branchActive || {},
407
+ history: activeHistorySnapshot(ws),
408
+ updated_at_ms: Date.now()
409
+ };
410
+ var previousSave = ws._branchSavePromise || Promise.resolve();
411
+ ws._branchSavePromise = previousSave.catch(function() {}).then(function() {
412
+ return fetch('/api/wall-e/chat/branches', {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json' },
415
+ body: JSON.stringify(payload)
416
+ });
417
+ }).then(function(resp) {
418
+ if (!resp.ok) {
419
+ // Surface the server's error message (e.g. a 413 "Body too large")
420
+ // instead of a bare status — the body carries { error, code }.
421
+ return resp.json().catch(function() { return null; }).then(function(body) {
422
+ var detail = body && body.error ? body.error : 'HTTP ' + resp.status;
423
+ throw new Error('Wall-E branch save failed: ' + detail);
424
+ });
425
+ }
426
+ ws._branchHistoryLoaded = true;
427
+ ws._branchActiveHistory = payload.history;
428
+ ws._branchSnapshotSavedAtMs = payload.updated_at_ms;
429
+ return true;
430
+ }).catch(function(err) {
431
+ console.warn('[walle-session] branch snapshot save failed:', err && err.message || err);
432
+ if (typeof toast === 'function') {
433
+ toast('Could not save Wall-E branch history: ' + (err && err.message ? err.message : err), { type: 'error', duration: 5000 });
434
+ }
435
+ return false;
436
+ });
437
+ return ws._branchSavePromise;
438
+ }
439
+
440
+ function applyLoadedBranchSnapshot(id, data) {
441
+ var ws = getState(id);
442
+ if (!ws || !data || typeof data !== 'object') return false;
443
+ var branches = normalizeBranchMap(data.branches || {});
444
+ var active = data.active && typeof data.active === 'object' ? data.active : {};
445
+ var history = normalizeBranchTail(data.history || []);
446
+ var snapshotSavedAtMs = Number(data.updated_at_ms || data.updatedAtMs || data.saved_at_ms || data.savedAtMs || 0) || 0;
447
+ if (!history.length && Object.keys(branches).length === 0) {
448
+ ws._branchHistoryLoaded = true;
449
+ if (data.exists === true) {
450
+ ws.branches = {};
451
+ ws.branchActive = {};
452
+ ws._branchActiveHistory = [];
453
+ ws._branchHistoryAuthoritative = false;
454
+ ws._branchSnapshotSavedAtMs = snapshotSavedAtMs;
455
+ }
456
+ return false;
457
+ }
458
+ ws.branches = branches;
459
+ ws.branchActive = {};
460
+ Object.keys(active).forEach(function(key) {
461
+ var n = Number(active[key]);
462
+ if (Number.isFinite(n)) ws.branchActive[String(parseInt(key, 10))] = n;
463
+ });
464
+ if (history.length) {
465
+ ws.messages = reconcileBranchHistoryWithDurable(history, activeHistorySnapshot(ws), snapshotSavedAtMs);
466
+ ws.messageCount = ws.messages.length;
467
+ ws._branchActiveHistory = ws.messages.map(cloneBranchMessage);
468
+ ws._branchHistoryAuthoritative = true;
469
+ } else {
470
+ ws._branchActiveHistory = activeHistorySnapshot(ws);
471
+ ws._branchHistoryAuthoritative = branchKeys(ws).length > 0;
472
+ }
473
+ ws._branchSnapshotSavedAtMs = snapshotSavedAtMs;
474
+ ws._branchHistoryLoaded = true;
475
+ return history.length > 0 || branchKeys(ws).length > 0;
476
+ }
477
+
478
+ function ensureWalleBranchSnapshotLoaded(id) {
479
+ var ws = getState(id);
480
+ if (!ws || ws._branchLoadStarted) return;
481
+ ws._branchLoadStarted = true;
482
+ fetch(branchApiUrl(walleBranchSessionId(id)), { cache: 'no-store' })
483
+ .then(function(resp) {
484
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
485
+ return resp.json();
486
+ })
487
+ .then(function(json) {
488
+ var changed = applyLoadedBranchSnapshot(id, json && (json.data || json));
489
+ var latest = getState(id);
490
+ if (changed && latest && document.getElementById('walle-session-' + id)) {
491
+ latest.renderPending = true;
492
+ if (!isWalleSessionVisible(id)) return;
493
+ var composerSnapshot = captureComposerState(id);
494
+ renderSession(id);
495
+ restoreComposerState(id, composerSnapshot);
496
+ }
497
+ })
498
+ .catch(function(err) {
499
+ console.warn('[walle-session] branch snapshot load failed:', err && err.message || err);
500
+ })
501
+ .finally(function() {
502
+ var latest = getState(id);
503
+ if (latest) latest._branchHistoryLoaded = true;
504
+ });
505
+ }
506
+
507
+ function messageContentForContext(msg) {
508
+ if (!msg) return '';
509
+ if (typeof msg.content === 'string') return msg.content;
510
+ if (typeof msg.text === 'string') return msg.text;
511
+ return String(msg.content || '');
512
+ }
513
+
514
+ function contextMessagesForModel(ws, outboundText) {
515
+ var messages = ((ws && ws.messages) || [])
516
+ .map(function(msg) {
517
+ var role = String(msg && msg.role || '').trim();
518
+ if (role !== 'user' && role !== 'assistant') return null;
519
+ var out = {
520
+ role: role,
521
+ content: messageContentForContext(msg)
522
+ };
523
+ if (Array.isArray(msg.attachments) && msg.attachments.length) out.attachments = cloneJsonSafe(msg.attachments, []);
524
+ return out;
525
+ })
526
+ .filter(function(msg) { return !!msg && (!!msg.content || (msg.attachments && msg.attachments.length)); });
527
+ if (outboundText && messages.length) {
528
+ for (var i = messages.length - 1; i >= 0; i--) {
529
+ if (messages[i].role === 'user') {
530
+ messages[i].content = outboundText;
531
+ break;
532
+ }
533
+ }
534
+ }
535
+ return messages.slice(-60);
536
+ }
537
+
111
538
  var _walleModelRegistryCache = null;
112
539
  var _walleModelRegistryLoaded = false;
113
540
  var _walleModelRegistryPromise = null;
@@ -497,15 +924,104 @@ window.WalleSession = (function() {
497
924
  { label: 'Find a bug', prompt: 'Run the test suite and help me debug whatever fails.' },
498
925
  { label: 'Paste a screenshot', prompt: 'I will paste a screenshot — tell me what is wrong with the UI.' }
499
926
  ];
927
+ function _hasRenderableMessages(ws) {
928
+ return !!(ws && ((ws.messageCount || 0) > 0 || (ws.messages && ws.messages.length)));
929
+ }
930
+ function historyStatusForSession(id, ws) {
931
+ var s = state.sessions.get(id);
932
+ if (_hasRenderableMessages(ws)) return 'loaded';
933
+ if (ws && ws.historyStatus === 'error') return 'error';
934
+ if ((ws && ws.historyStatus === 'loaded') || (s && s._walleHistoryLoaded)) return 'loaded';
935
+ if ((ws && ws.historyStatus === 'loading') ||
936
+ (s && (s.needsAttach || s._walleHistoryLoading || (s._walleHistoryRequested && !s._walleHistoryLoaded)))) {
937
+ return 'loading';
938
+ }
939
+ return 'idle';
940
+ }
941
+ function markHistoryLoading(id, reason) {
942
+ var s = state.sessions.get(id);
943
+ var ws = getState(id);
944
+ if (!s || !ws) return false;
945
+ ws.historyRequestSeq = (Number(ws.historyRequestSeq || 0) || 0) + 1;
946
+ ws.historyStatus = 'loading';
947
+ ws.historyRequestedAt = Date.now();
948
+ ws.historyLoadReason = reason || 'attach';
949
+ ws.historyRequestId = ws.historyRequestSeq;
950
+ s._walleHistoryRequested = true;
951
+ s._walleHistoryRequestedAt = ws.historyRequestedAt;
952
+ s._walleHistoryRequestSeq = ws.historyRequestSeq;
953
+ s._walleHistoryLoading = true;
954
+ s._walleHistoryLoaded = false;
955
+ var messagesArea = document.getElementById('walle-messages-' + id);
956
+ if (messagesArea && !_hasRenderableMessages(ws)) {
957
+ _renderEmptyStateChipsIfNeeded(id, messagesArea);
958
+ }
959
+ updateHeaderStats(id);
960
+ return true;
961
+ }
962
+
963
+ function shouldRequestHistory(id, opts) {
964
+ opts = opts || {};
965
+ var s = state.sessions.get(id);
966
+ var ws = getState(id);
967
+ if (!s || !ws) return false;
968
+ if (opts.force === true) return true;
969
+ if (_hasRenderableMessages(ws)) return false;
970
+ if (ws.historyStatus === 'loaded' || s._walleHistoryLoaded) return false;
971
+ var requested = !!(s._walleHistoryRequested || ws.historyStatus === 'loading' || s._walleHistoryLoading);
972
+ if (!requested) return true;
973
+ var requestedAt = Number(ws.historyRequestedAt || s._walleHistoryRequestedAt || 0);
974
+ if (!requestedAt) return true;
975
+ return Date.now() - requestedAt >= HISTORY_RETRY_AFTER_MS;
976
+ }
977
+
978
+ function requestHistory(id, reason, opts) {
979
+ opts = opts || {};
980
+ if (!shouldRequestHistory(id, opts)) return false;
981
+ var s = state.sessions.get(id);
982
+ var ws = getState(id);
983
+ if (!s || !ws) return false;
984
+ s.needsAttach = false;
985
+ if (!markHistoryLoading(id, reason || 'walle-history-request')) return false;
986
+ send({
987
+ type: 'walle-history-request',
988
+ id: id,
989
+ reason: reason || 'walle-history-request',
990
+ historyRequestId: ws.historyRequestId,
991
+ limit: opts.limit || 500,
992
+ noCache: opts.noCache === true,
993
+ });
994
+ return true;
995
+ }
996
+ function markHistoryLoaded(id) {
997
+ var s = state.sessions.get(id);
998
+ var ws = getState(id);
999
+ if (!s || !ws) return false;
1000
+ ws.historyStatus = 'loaded';
1001
+ ws.historyLoadedAt = Date.now();
1002
+ ws.historyLoadReason = '';
1003
+ s._walleHistoryRequested = true;
1004
+ s._walleHistoryLoading = false;
1005
+ s._walleHistoryLoaded = true;
1006
+ s._walleHistoryLoadedAt = ws.historyLoadedAt;
1007
+ return true;
1008
+ }
500
1009
  function _renderEmptyStateChipsIfNeeded(id, messagesArea) {
501
1010
  if (!messagesArea) return;
502
1011
  var ws = getState(id);
503
1012
  if (!ws) return;
504
1013
  // Don't render if there are already messages in this session.
505
- if ((ws.messageCount || 0) > 0 || (ws.messages && ws.messages.length)) {
1014
+ if (_hasRenderableMessages(ws)) {
1015
+ _removeEmptyAndLoadingState(messagesArea);
1016
+ return;
1017
+ }
1018
+ var historyStatus = historyStatusForSession(id, ws);
1019
+ if (historyStatus === 'loading') {
506
1020
  _removeEmptyStateChips(messagesArea);
1021
+ _renderHistoryLoadingState(messagesArea);
507
1022
  return;
508
1023
  }
1024
+ _removeHistoryLoadingState(messagesArea);
509
1025
  if (messagesArea.querySelector('.walle-empty-state')) return;
510
1026
  var wrap = document.createElement('div');
511
1027
  wrap.className = 'walle-empty-state';
@@ -535,11 +1051,58 @@ window.WalleSession = (function() {
535
1051
  wrap.appendChild(chips);
536
1052
  messagesArea.appendChild(wrap);
537
1053
  }
1054
+ function _renderHistoryLoadingState(messagesArea) {
1055
+ if (!messagesArea || messagesArea.querySelector('.walle-history-loading-state')) return;
1056
+ var wrap = document.createElement('div');
1057
+ wrap.className = 'walle-history-loading-state';
1058
+ wrap.setAttribute('role', 'status');
1059
+ wrap.setAttribute('aria-live', 'polite');
1060
+
1061
+ var badge = document.createElement('div');
1062
+ badge.className = 'walle-history-loading-badge';
1063
+ var spinner = document.createElement('span');
1064
+ spinner.className = 'walle-history-loading-spinner';
1065
+ spinner.setAttribute('aria-hidden', 'true');
1066
+ badge.appendChild(spinner);
1067
+ var badgeText = document.createElement('span');
1068
+ badgeText.textContent = 'Loading';
1069
+ badge.appendChild(badgeText);
1070
+ wrap.appendChild(badge);
1071
+
1072
+ var title = document.createElement('div');
1073
+ title.className = 'walle-history-loading-title';
1074
+ title.textContent = 'Rehydrating Wall-E session history';
1075
+ wrap.appendChild(title);
1076
+
1077
+ var copy = document.createElement('div');
1078
+ copy.className = 'walle-history-loading-copy';
1079
+ copy.textContent = 'Messages are being replayed after restart. You can keep typing while CTM catches up.';
1080
+ wrap.appendChild(copy);
1081
+
1082
+ var skeleton = document.createElement('div');
1083
+ skeleton.className = 'walle-history-loading-skeleton';
1084
+ for (var i = 0; i < 3; i++) {
1085
+ var row = document.createElement('span');
1086
+ row.className = 'walle-history-loading-line';
1087
+ skeleton.appendChild(row);
1088
+ }
1089
+ wrap.appendChild(skeleton);
1090
+ messagesArea.appendChild(wrap);
1091
+ }
1092
+ function _removeHistoryLoadingState(messagesArea) {
1093
+ if (!messagesArea) return;
1094
+ var ex = messagesArea.querySelector('.walle-history-loading-state');
1095
+ if (ex && ex.parentNode) ex.parentNode.removeChild(ex);
1096
+ }
538
1097
  function _removeEmptyStateChips(messagesArea) {
539
1098
  if (!messagesArea) return;
540
1099
  var ex = messagesArea.querySelector('.walle-empty-state');
541
1100
  if (ex && ex.parentNode) ex.parentNode.removeChild(ex);
542
1101
  }
1102
+ function _removeEmptyAndLoadingState(messagesArea) {
1103
+ _removeEmptyStateChips(messagesArea);
1104
+ _removeHistoryLoadingState(messagesArea);
1105
+ }
543
1106
 
544
1107
  function tokenParam() {
545
1108
  return state && state.token ? '&token=' + encodeURIComponent(state.token) : '';
@@ -607,10 +1170,17 @@ window.WalleSession = (function() {
607
1170
  ws.inputDraft = '';
608
1171
  }
609
1172
 
610
- function canRecallHistory(textarea, key) {
611
- var multiline = (textarea.value || '').indexOf('\n') >= 0;
612
- if (key === 'ArrowUp') return !multiline || textarea.selectionStart === 0;
613
- if (key === 'ArrowDown') return !multiline || textarea.selectionEnd === textarea.value.length;
1173
+ function canRecallHistory(textarea, key, browsingHistory) {
1174
+ if (!textarea) return false;
1175
+ var value = textarea.value || '';
1176
+ var start = typeof textarea.selectionStart === 'number' ? textarea.selectionStart : value.length;
1177
+ var end = typeof textarea.selectionEnd === 'number' ? textarea.selectionEnd : start;
1178
+ if (start !== end) return false;
1179
+ var atStart = start === 0;
1180
+ var atEnd = end === value.length;
1181
+ var singleLine = value.indexOf('\n') < 0;
1182
+ if (key === 'ArrowUp') return atStart || (atEnd && (singleLine || browsingHistory));
1183
+ if (key === 'ArrowDown') return atEnd;
614
1184
  return false;
615
1185
  }
616
1186
 
@@ -678,18 +1248,17 @@ window.WalleSession = (function() {
678
1248
  if (!isFinite(minHeight) || minHeight <= 0) minHeight = 44;
679
1249
  if (!isFinite(maxHeight) || maxHeight <= 0) maxHeight = 260;
680
1250
  textarea.style.height = 'auto';
1251
+ var preferredHeight = null;
1252
+ var effectiveMaxHeight = maxHeight;
681
1253
  if (ws && ws.composerHeightPx != null) {
682
- var fixedHeight = clampComposerHeight(id, textarea, ws.composerHeightPx);
683
- textarea.style.height = fixedHeight + 'px';
684
- var fixedScrollable = textarea.scrollHeight > fixedHeight + 1;
685
- textarea.classList.toggle('is-scrollable', fixedScrollable);
686
- textarea.style.overflowY = fixedScrollable ? 'auto' : 'hidden';
687
- if (id) syncComposerResizeHandle(id, textarea);
688
- return;
1254
+ preferredHeight = clampComposerHeight(id, textarea, ws.composerHeightPx);
1255
+ var bounds = composerHeightBounds(id, textarea);
1256
+ effectiveMaxHeight = Math.min(bounds.max, Math.max(maxHeight, preferredHeight));
689
1257
  }
690
- var next = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight));
1258
+ var next = Math.max(minHeight, Math.min(textarea.scrollHeight, effectiveMaxHeight));
1259
+ if (preferredHeight != null) next = Math.min(effectiveMaxHeight, Math.max(preferredHeight, next));
691
1260
  textarea.style.height = next + 'px';
692
- var scrollable = textarea.scrollHeight > maxHeight + 1;
1261
+ var scrollable = textarea.scrollHeight > next + 1;
693
1262
  textarea.classList.toggle('is-scrollable', scrollable);
694
1263
  textarea.style.overflowY = scrollable ? 'auto' : 'hidden';
695
1264
  if (id) syncComposerResizeHandle(id, textarea);
@@ -720,7 +1289,68 @@ window.WalleSession = (function() {
720
1289
  }
721
1290
  }
722
1291
 
1292
+ function snapshotComposerTextarea(textarea) {
1293
+ if (!textarea) return null;
1294
+ var value = textarea.value || '';
1295
+ var start = typeof textarea.selectionStart === 'number' ? textarea.selectionStart : value.length;
1296
+ var end = typeof textarea.selectionEnd === 'number' ? textarea.selectionEnd : start;
1297
+ start = Math.max(0, Math.min(start, value.length));
1298
+ end = Math.max(0, Math.min(end, value.length));
1299
+ return {
1300
+ value: value,
1301
+ selectionStart: start,
1302
+ selectionEnd: end,
1303
+ focused: document.activeElement === textarea,
1304
+ updatedAt: Date.now()
1305
+ };
1306
+ }
1307
+
1308
+ function cloneComposerSnapshot(snapshot) {
1309
+ if (!snapshot || typeof snapshot !== 'object') return null;
1310
+ var value = typeof snapshot.value === 'string' ? snapshot.value : '';
1311
+ var start = typeof snapshot.selectionStart === 'number' ? snapshot.selectionStart : value.length;
1312
+ var end = typeof snapshot.selectionEnd === 'number' ? snapshot.selectionEnd : start;
1313
+ start = Math.max(0, Math.min(start, value.length));
1314
+ end = Math.max(0, Math.min(end, value.length));
1315
+ return {
1316
+ value: value,
1317
+ selectionStart: start,
1318
+ selectionEnd: end,
1319
+ focused: snapshot.focused === true,
1320
+ updatedAt: typeof snapshot.updatedAt === 'number' ? snapshot.updatedAt : Date.now()
1321
+ };
1322
+ }
1323
+
1324
+ function rememberComposerSnapshot(id, snapshot) {
1325
+ var ws = getState(id);
1326
+ var normalized = cloneComposerSnapshot(snapshot);
1327
+ if (!ws || !normalized) return normalized;
1328
+ ws.composerDraft = normalized;
1329
+ return normalized;
1330
+ }
1331
+
1332
+ function rememberComposerTextarea(id, textarea) {
1333
+ if (!textarea) return null;
1334
+ return rememberComposerSnapshot(id, snapshotComposerTextarea(textarea));
1335
+ }
1336
+
1337
+ function storedComposerSnapshot(id) {
1338
+ var ws = getState(id);
1339
+ return ws ? cloneComposerSnapshot(ws.composerDraft) : null;
1340
+ }
1341
+
1342
+ function clearComposerDraft(id, textarea) {
1343
+ var snapshot = { value: '', selectionStart: 0, selectionEnd: 0, focused: !!(textarea && document.activeElement === textarea), updatedAt: Date.now() };
1344
+ rememberComposerSnapshot(id, snapshot);
1345
+ var ws = getState(id);
1346
+ if (ws) {
1347
+ ws.inputHistoryIdx = -1;
1348
+ ws.inputDraft = '';
1349
+ }
1350
+ }
1351
+
723
1352
  function syncComposerChrome(id, textarea) {
1353
+ rememberComposerTextarea(id, textarea);
724
1354
  resizeComposerTextarea(textarea);
725
1355
  syncComposerPreview(id);
726
1356
  }
@@ -734,7 +1364,7 @@ window.WalleSession = (function() {
734
1364
  handle.setAttribute('aria-label', 'Resize Wall-E composer');
735
1365
  handle.setAttribute('aria-controls', 'walle-input-' + id);
736
1366
  handle.tabIndex = 0;
737
- handle.title = 'Drag to resize composer. Double-click to reset.';
1367
+ handle.title = 'Drag to set the minimum composer height. Double-click to reset.';
738
1368
  var grip = document.createElement('span');
739
1369
  grip.className = 'walle-composer-resize-grip';
740
1370
  grip.setAttribute('aria-hidden', 'true');
@@ -811,28 +1441,25 @@ window.WalleSession = (function() {
811
1441
  var root = document.getElementById('walle-session-' + id);
812
1442
  var textarea = root && root.querySelector ? root.querySelector('.walle-input') : null;
813
1443
  if (!textarea) return null;
814
- return {
815
- value: textarea.value || '',
816
- selectionStart: textarea.selectionStart || 0,
817
- selectionEnd: textarea.selectionEnd || textarea.selectionStart || 0,
818
- focused: document.activeElement === textarea,
819
- };
1444
+ return rememberComposerTextarea(id, textarea);
820
1445
  }
821
1446
 
822
1447
  function restoreComposerState(id, snapshot) {
823
- if (!snapshot) return;
1448
+ var restore = cloneComposerSnapshot(snapshot) || storedComposerSnapshot(id);
1449
+ if (!restore) return;
824
1450
  var root = document.getElementById('walle-session-' + id);
825
1451
  var textarea = root && root.querySelector ? root.querySelector('.walle-input') : null;
826
1452
  if (!textarea) return;
827
- textarea.value = snapshot.value || '';
1453
+ textarea.value = restore.value || '';
828
1454
  syncComposerChrome(id, textarea);
829
1455
  try {
830
1456
  textarea.setSelectionRange(
831
- Math.min(snapshot.selectionStart || 0, textarea.value.length),
832
- Math.min(snapshot.selectionEnd || snapshot.selectionStart || 0, textarea.value.length)
1457
+ Math.min(restore.selectionStart || 0, textarea.value.length),
1458
+ Math.min(restore.selectionEnd || restore.selectionStart || 0, textarea.value.length)
833
1459
  );
834
1460
  } catch (_) {}
835
- if (snapshot.focused) {
1461
+ rememberComposerTextarea(id, textarea);
1462
+ if (restore.focused) {
836
1463
  try { textarea.focus(); } catch (_) {}
837
1464
  }
838
1465
  }
@@ -847,8 +1474,10 @@ window.WalleSession = (function() {
847
1474
  function recallInputHistory(id, textarea, dir) {
848
1475
  var ws = getState(id);
849
1476
  if (!ws || !textarea || !Array.isArray(ws.inputHistory) || ws.inputHistory.length === 0) return false;
1477
+ var browsingHistory = ws.inputHistoryIdx !== -1;
850
1478
  if (dir < 0) {
851
- if (!canRecallHistory(textarea, 'ArrowUp')) return false;
1479
+ if (!canRecallHistory(textarea, 'ArrowUp', browsingHistory)) return false;
1480
+ if (!browsingHistory && (textarea.value || '').length > 0) return false;
852
1481
  if (ws.inputHistoryIdx === -1) {
853
1482
  ws.inputDraft = textarea.value;
854
1483
  ws.inputHistoryIdx = 0;
@@ -858,7 +1487,7 @@ window.WalleSession = (function() {
858
1487
  setTextareaValue(textarea, ws.inputHistory[ws.inputHistoryIdx] || '');
859
1488
  return true;
860
1489
  }
861
- if (!canRecallHistory(textarea, 'ArrowDown') || ws.inputHistoryIdx === -1) return false;
1490
+ if (!canRecallHistory(textarea, 'ArrowDown', browsingHistory) || ws.inputHistoryIdx === -1) return false;
862
1491
  ws.inputHistoryIdx--;
863
1492
  if (ws.inputHistoryIdx < 0) {
864
1493
  ws.inputHistoryIdx = -1;
@@ -870,6 +1499,14 @@ window.WalleSession = (function() {
870
1499
  return true;
871
1500
  }
872
1501
 
1502
+ function shouldProtectDraftHistoryArrow(id, textarea, key) {
1503
+ var ws = getState(id);
1504
+ if (!ws || !textarea || !Array.isArray(ws.inputHistory) || ws.inputHistory.length === 0) return false;
1505
+ if (ws.inputHistoryIdx !== -1) return false;
1506
+ if (!(textarea.value || '').length) return false;
1507
+ return canRecallHistory(textarea, key, false);
1508
+ }
1509
+
873
1510
  function resetInputHistoryBrowse(ws) {
874
1511
  if (!ws) return;
875
1512
  ws.inputHistoryIdx = -1;
@@ -904,6 +1541,131 @@ window.WalleSession = (function() {
904
1541
  });
905
1542
  }
906
1543
 
1544
+ function isWalleSessionVisible(id) {
1545
+ var root = document.getElementById('walle-session-' + id);
1546
+ if (state && state.activeTab === id) return true;
1547
+ if (root && root.classList && root.classList.contains('active')) return true;
1548
+ try {
1549
+ if (typeof isSessionVisibleInSplit === 'function' && isSessionVisibleInSplit(id)) return true;
1550
+ } catch (_) {}
1551
+ return false;
1552
+ }
1553
+
1554
+ function nextFrame(fn) {
1555
+ if (typeof requestAnimationFrame === 'function') return requestAnimationFrame(fn);
1556
+ return setTimeout(fn, 16);
1557
+ }
1558
+
1559
+ function cancelFrame(handle) {
1560
+ if (!handle) return;
1561
+ try {
1562
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
1563
+ else clearTimeout(handle);
1564
+ } catch (_) {}
1565
+ }
1566
+
1567
+ function cancelMessageRender(ws) {
1568
+ if (!ws) return;
1569
+ ws.messageRenderEpoch = Number(ws.messageRenderEpoch || 0) + 1;
1570
+ cancelFrame(ws.messageRenderFrame);
1571
+ ws.messageRenderFrame = 0;
1572
+ ws.renderingMessages = false;
1573
+ }
1574
+
1575
+ function finalizeMessageRender(id, ws, messagesArea, textarea, epoch) {
1576
+ if (!ws || Number(ws.messageRenderEpoch || 0) !== epoch) return;
1577
+ ws.messageRenderFrame = 0;
1578
+ ws.renderingMessages = false;
1579
+ ws.renderPending = false;
1580
+ if (messagesArea) messagesArea.dataset.walleRenderState = 'complete';
1581
+ syncPromptMessageDomIndices(id);
1582
+ updatePromptNav(id);
1583
+ renderWorkBar(id);
1584
+ renderQueuePreview(id);
1585
+ renderComposerAttachments(id);
1586
+ if (textarea) syncComposerChrome(id, textarea);
1587
+ if (messagesArea && isWalleSessionVisible(id)) scrollToBottom(messagesArea);
1588
+ focusInput(id);
1589
+ }
1590
+
1591
+ function renderMessagesIncrementally(id, ws, messagesArea, textarea) {
1592
+ if (!ws || !messagesArea) return;
1593
+ cancelMessageRender(ws);
1594
+ messagesArea.dataset.lastDate = '';
1595
+ messagesArea.dataset.walleRenderState = 'idle';
1596
+
1597
+ var messages = Array.isArray(ws.messages) ? ws.messages.slice() : [];
1598
+ if (!messages.length) {
1599
+ finalizeMessageRender(id, ws, messagesArea, textarea, Number(ws.messageRenderEpoch || 0));
1600
+ return;
1601
+ }
1602
+
1603
+ var epoch = Number(ws.messageRenderEpoch || 0) + 1;
1604
+ ws.messageRenderEpoch = epoch;
1605
+ ws.renderPending = false;
1606
+ ws.renderingMessages = messages.length > MESSAGE_RENDER_SYNC_LIMIT;
1607
+
1608
+ var index = 0;
1609
+ var renderOne = function() {
1610
+ renderMessage(messagesArea, messages[index], index);
1611
+ index += 1;
1612
+ };
1613
+
1614
+ if (messages.length <= MESSAGE_RENDER_SYNC_LIMIT) {
1615
+ while (index < messages.length) renderOne();
1616
+ finalizeMessageRender(id, ws, messagesArea, textarea, epoch);
1617
+ return;
1618
+ }
1619
+
1620
+ messagesArea.dataset.walleRenderState = 'rendering';
1621
+ var appendBatch = function() {
1622
+ if (!ws || Number(ws.messageRenderEpoch || 0) !== epoch || !messagesArea.isConnected) return;
1623
+ if (!isWalleSessionVisible(id)) {
1624
+ ws.messageRenderFrame = 0;
1625
+ ws.renderingMessages = false;
1626
+ ws.renderPending = true;
1627
+ messagesArea.dataset.walleRenderState = 'paused';
1628
+ return;
1629
+ }
1630
+
1631
+ var started = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
1632
+ var rendered = 0;
1633
+ while (index < messages.length && rendered < MESSAGE_RENDER_BATCH_SIZE) {
1634
+ renderOne();
1635
+ rendered += 1;
1636
+ var now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
1637
+ if (now - started >= MESSAGE_RENDER_FRAME_BUDGET_MS) break;
1638
+ }
1639
+
1640
+ if (messagesArea && isWalleSessionVisible(id)) scrollToBottom(messagesArea);
1641
+ if (index < messages.length) {
1642
+ ws.messageRenderFrame = nextFrame(appendBatch);
1643
+ return;
1644
+ }
1645
+ finalizeMessageRender(id, ws, messagesArea, textarea, epoch);
1646
+ // Re-attach inline queued-prompt bubbles after a full message re-render.
1647
+ renderPendingQueueBubbles(id);
1648
+ };
1649
+
1650
+ appendBatch();
1651
+ }
1652
+
1653
+ function messageRenderNeedsRestart(id) {
1654
+ var ws = getState(id);
1655
+ return !!(ws && ws.renderPending);
1656
+ }
1657
+
1658
+ function restartVisibleMessageRenderIfBusy(id, ws) {
1659
+ if (!ws || !ws.renderingMessages) return false;
1660
+ ws.renderPending = true;
1661
+ if (isWalleSessionVisible(id)) {
1662
+ var composerSnapshot = captureComposerState(id);
1663
+ renderSession(id);
1664
+ restoreComposerState(id, composerSnapshot);
1665
+ }
1666
+ return true;
1667
+ }
1668
+
907
1669
  function isComposerTextTarget(target) {
908
1670
  if (!target || !target.closest) return false;
909
1671
  return !!target.closest('input, textarea, select, button, a, [contenteditable="true"], [contenteditable=""]');
@@ -1147,6 +1909,10 @@ window.WalleSession = (function() {
1147
1909
 
1148
1910
  function redirectComposerKeydown(id, e) {
1149
1911
  if (!e || isComposerTextTarget(e.target)) return;
1912
+ if (e.key === 'Escape') {
1913
+ confirmCancelActiveRun(id, e);
1914
+ return;
1915
+ }
1150
1916
  if (isMacCtrlVPasteShortcut(e)) {
1151
1917
  var pasteContainer = document.getElementById('walle-session-' + id);
1152
1918
  var pasteTextarea = pasteContainer && pasteContainer.querySelector('.walle-input');
@@ -1230,8 +1996,43 @@ window.WalleSession = (function() {
1230
1996
  var a = attachments[i] || {};
1231
1997
  var thumb = document.createElement('button');
1232
1998
  thumb.type = 'button';
1233
- thumb.className = 'walle-attachment-thumb';
1234
1999
  var label = a.label || ('[Image #' + (i + 1) + ']');
2000
+ if (a.pending) {
2001
+ // Optimistic placeholder while the image resizes+compresses.
2002
+ thumb.className = 'walle-attachment-thumb walle-attachment-thumb--pending';
2003
+ thumb.disabled = true;
2004
+ thumb.title = label + ' — compressing…';
2005
+ var spinner = document.createElement('span');
2006
+ spinner.className = 'walle-attachment-spinner';
2007
+ spinner.setAttribute('aria-hidden', 'true');
2008
+ thumb.appendChild(spinner);
2009
+ var pendingToken = document.createElement('span');
2010
+ pendingToken.className = 'walle-attachment-token';
2011
+ pendingToken.textContent = 'compressing…';
2012
+ thumb.appendChild(pendingToken);
2013
+ if (editable) {
2014
+ var pendingRemove = document.createElement('span');
2015
+ pendingRemove.className = 'walle-attachment-remove';
2016
+ pendingRemove.textContent = '×';
2017
+ pendingRemove.title = 'Remove image';
2018
+ pendingRemove.addEventListener('click', (function(idx) {
2019
+ return function(e) {
2020
+ e.preventDefault();
2021
+ e.stopPropagation();
2022
+ var ws = getState(id);
2023
+ if (ws && Array.isArray(ws.inputAttachments)) {
2024
+ ws.inputAttachments.splice(idx, 1);
2025
+ renderComposerAttachments(id);
2026
+ updateSendButton(id, !!ws.isGenerating);
2027
+ }
2028
+ };
2029
+ })(i));
2030
+ thumb.appendChild(pendingRemove);
2031
+ }
2032
+ strip.appendChild(thumb);
2033
+ continue;
2034
+ }
2035
+ thumb.className = 'walle-attachment-thumb';
1235
2036
  thumb.title = label + ' — click to preview, right-click for path or URL';
1236
2037
  thumb.dataset.index = String(i);
1237
2038
  var img = document.createElement('img');
@@ -1326,20 +2127,50 @@ window.WalleSession = (function() {
1326
2127
  });
1327
2128
  }
1328
2129
 
2130
+ function hasPendingAttachments(ws) {
2131
+ return !!(ws && Array.isArray(ws.inputAttachments)
2132
+ && ws.inputAttachments.some(function(a) { return a && a.pending; }));
2133
+ }
2134
+
1329
2135
  async function attachImageBlob(id, blob, filename) {
1330
2136
  var ws = getState(id);
1331
2137
  if (!ws || !blob) return;
1332
- var dataUrl = await readBlobAsDataUrl(blob);
1333
- var img = await uploadAttachmentBlob(blob, filename);
1334
- img.label = nextAttachmentLabel(ws);
1335
- img.type = 'image';
1336
- img.name = img.filename || sanitizeImageFilename(filename, extensionForMime(blob.type));
1337
- img.mediaType = blob.type || 'image/png';
1338
- img.data = dataUrl;
1339
- ws.inputAttachments.push(img);
1340
- insertAttachmentToken(id, img.label);
2138
+ var label = nextAttachmentLabel(ws);
2139
+ var safeName = sanitizeImageFilename(filename, extensionForMime(blob.type));
2140
+ // Show an optimistic "compressing…" chip the instant the image is pasted and
2141
+ // gate send until it's resized+compressed (a large screenshot takes ~0.5-1s),
2142
+ // so a fast Enter can't fire the message before the attachment is attached.
2143
+ var pendingKey = 'pending-' + Date.now() + '-' + Math.floor(Math.random() * 1e9);
2144
+ ws.inputAttachments.push({
2145
+ type: 'image', label: label, name: safeName,
2146
+ mediaType: blob.type || 'image/png', pending: true, _pendingKey: pendingKey,
2147
+ });
2148
+ insertAttachmentToken(id, label);
1341
2149
  renderComposerAttachments(id);
2150
+ updateSendButton(id, !!ws.isGenerating);
1342
2151
  focusInput(id);
2152
+ try {
2153
+ var dataUrl = await readBlobAsDataUrl(blob);
2154
+ var img = await uploadAttachmentBlob(blob, filename);
2155
+ img.label = label;
2156
+ img.type = 'image';
2157
+ img.name = img.filename || safeName;
2158
+ img.mediaType = blob.type || 'image/png';
2159
+ img.data = dataUrl;
2160
+ img.pending = false;
2161
+ // Replace the placeholder in place. If the user removed it while it was
2162
+ // processing, honor that and don't re-add the image.
2163
+ var idx = ws.inputAttachments.findIndex(function(a) { return a && a._pendingKey === pendingKey; });
2164
+ if (idx >= 0) ws.inputAttachments[idx] = img;
2165
+ renderComposerAttachments(id);
2166
+ } catch (err) {
2167
+ var failIdx = ws.inputAttachments.findIndex(function(a) { return a && a._pendingKey === pendingKey; });
2168
+ if (failIdx >= 0) ws.inputAttachments.splice(failIdx, 1);
2169
+ renderComposerAttachments(id);
2170
+ throw err;
2171
+ } finally {
2172
+ updateSendButton(id, !!ws.isGenerating);
2173
+ }
1343
2174
  }
1344
2175
 
1345
2176
  async function handlePaste(id, e) {
@@ -1484,7 +2315,9 @@ window.WalleSession = (function() {
1484
2315
  function renderSession(id) {
1485
2316
  var s = state.sessions.get(id);
1486
2317
  if (!s) return;
2318
+ var composerSnapshot = captureComposerState(id);
1487
2319
  var ws = getState(id);
2320
+ var initialComposerSnapshot = composerSnapshot || storedComposerSnapshot(id);
1488
2321
 
1489
2322
  var container = s.container || document.getElementById('walle-session-' + id);
1490
2323
  if (!container) return;
@@ -1505,7 +2338,9 @@ window.WalleSession = (function() {
1505
2338
  info.className = 'session-info';
1506
2339
  var title = document.createElement('div');
1507
2340
  title.className = 'session-title';
1508
- title.textContent = s.meta?.label || 'Wall-E';
2341
+ title.textContent = (typeof activeSessionDisplayLabel === 'function'
2342
+ ? activeSessionDisplayLabel(s, id)
2343
+ : (s.meta?.displayTitle || s.meta?.aiTitle || s.meta?.title || s.meta?.label)) || 'Wall-E';
1509
2344
  info.appendChild(title);
1510
2345
 
1511
2346
  var meta = document.createElement('div');
@@ -1513,7 +2348,24 @@ window.WalleSession = (function() {
1513
2348
  meta.id = 'walle-meta-' + id;
1514
2349
  var cwd = s.meta?.cwd || '';
1515
2350
  var msgCount = ws.messageCount || 0;
1516
- meta.textContent = (cwd ? cwd + ' ' : '') + msgCount + ' messages';
2351
+ // Working directory is editable: clicking it opens an inline input that
2352
+ // sends `update-session-cwd`. A Wall-E chat session is bound to the dir it
2353
+ // was created in; when the user wants work to happen elsewhere (e.g. "build
2354
+ // it under ~/ws/sl-web, don't touch ~/ws/octo") the model otherwise has to
2355
+ // prefix every run_shell with `cd <target>`, which trips the `cd * -> ask`
2356
+ // permission rule on every command. Setting the cwd here points the coding
2357
+ // runtime (effectiveCwd) at the intended dir so no `cd` prefix is needed.
2358
+ var cwdEl = renderSessionCwdControl(id, cwd);
2359
+ meta.appendChild(cwdEl);
2360
+ var countText = document.createElement('span');
2361
+ countText.className = 'session-meta-count';
2362
+ countText.style.marginLeft = cwd ? '8px' : '0';
2363
+ if (historyStatusForSession(id, ws) === 'loading' && msgCount === 0) {
2364
+ countText.textContent = 'Loading messages...';
2365
+ } else {
2366
+ countText.textContent = msgCount + ' messages';
2367
+ }
2368
+ meta.appendChild(countText);
1517
2369
  info.appendChild(meta);
1518
2370
  header.appendChild(info);
1519
2371
 
@@ -1606,6 +2458,11 @@ window.WalleSession = (function() {
1606
2458
  messagesArea.id = 'walle-messages-' + id;
1607
2459
  messagesArea.tabIndex = -1;
1608
2460
 
2461
+ var workbarSlot = document.createElement('div');
2462
+ workbarSlot.className = 'walle-workbar-slot';
2463
+ workbarSlot.id = 'walle-workbar-' + id;
2464
+ workbarSlot.style.display = 'none';
2465
+
1609
2466
  // Input bar
1610
2467
  var inputBar = document.createElement('div');
1611
2468
  inputBar.className = 'walle-input-bar';
@@ -1613,6 +2470,11 @@ window.WalleSession = (function() {
1613
2470
  var inputStack = document.createElement('div');
1614
2471
  inputStack.className = 'walle-input-stack';
1615
2472
 
2473
+ var queuePreview = document.createElement('div');
2474
+ queuePreview.className = 'walle-queue-preview';
2475
+ queuePreview.id = 'walle-queue-preview-' + id;
2476
+ inputStack.appendChild(queuePreview);
2477
+
1616
2478
  var attachmentPreview = document.createElement('div');
1617
2479
  attachmentPreview.className = 'walle-composer-attachments';
1618
2480
  attachmentPreview.id = 'walle-attachments-' + id;
@@ -1628,6 +2490,9 @@ window.WalleSession = (function() {
1628
2490
  textarea.id = 'walle-input-' + id;
1629
2491
  textarea.placeholder = 'Message Wall-E... paste images here';
1630
2492
  textarea.rows = 1;
2493
+ if (initialComposerSnapshot && initialComposerSnapshot.value) {
2494
+ textarea.value = initialComposerSnapshot.value;
2495
+ }
1631
2496
  textarea.dataset.skillAgent = 'walle';
1632
2497
  textarea.dataset.skillCwd = s.meta?.cwd || s.meta?.project_path || s.meta?.project || '';
1633
2498
  textarea.dataset.skillMode = 'walle-composer';
@@ -1649,23 +2514,22 @@ window.WalleSession = (function() {
1649
2514
  e.preventDefault();
1650
2515
  return;
1651
2516
  }
1652
- }
1653
- if (e.key === 'Escape') {
1654
- var ws = getState(id);
1655
- if (ws && ws.isGenerating) {
2517
+ if (shouldProtectDraftHistoryArrow(id, textarea, e.key)) {
1656
2518
  e.preventDefault();
1657
- send({ type: 'walle-cancel', id: id });
2519
+ rememberComposerTextarea(id, textarea);
2520
+ return;
1658
2521
  }
1659
- return;
2522
+ }
2523
+ if (e.key === 'Escape') {
2524
+ confirmCancelActiveRun(id, e);
2525
+ return;
1660
2526
  }
1661
2527
  if (e.key === 'Enter' && !e.shiftKey) {
1662
- // Plain Enter sends. Shift+Enter inserts newline.
1663
- // Cmd/Ctrl+Enter also sends (Slack/Cursor muscle memory).
2528
+ // Shift+Enter inserts a newline. ⌘/Ctrl+Enter = "send now" (interrupt a
2529
+ // busy run and send next); plain Enter = send, or queue when busy.
1664
2530
  e.preventDefault();
1665
- sendMessage(id);
1666
- } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1667
- e.preventDefault();
1668
- sendMessage(id);
2531
+ if (e.metaKey || e.ctrlKey) sendNow(id);
2532
+ else sendMessage(id);
1669
2533
  }
1670
2534
  });
1671
2535
  textarea.addEventListener('input', function() {
@@ -1677,7 +2541,11 @@ window.WalleSession = (function() {
1677
2541
  pe.checkSlashTriggerAtCursor(textarea);
1678
2542
  }
1679
2543
  });
2544
+ textarea.addEventListener('keyup', function() { rememberComposerTextarea(id, textarea); });
2545
+ textarea.addEventListener('click', function() { rememberComposerTextarea(id, textarea); });
2546
+ textarea.addEventListener('select', function() { rememberComposerTextarea(id, textarea); });
1680
2547
  textarea.addEventListener('blur', function() {
2548
+ rememberComposerTextarea(id, textarea);
1681
2549
  var pe = promptEditorApi();
1682
2550
  if (!pe || typeof pe.hideSlashPicker !== 'function') return;
1683
2551
  setTimeout(function() {
@@ -1716,13 +2584,14 @@ window.WalleSession = (function() {
1716
2584
  // send key in Sprint 1). Mac users see ⌘; everyone else sees Ctrl.
1717
2585
  var isMac = (navigator.platform || '').toUpperCase().indexOf('MAC') >= 0;
1718
2586
  var sendChord = isMac ? '⌘' : 'Ctrl';
1719
- hints.textContent = 'Enter or ' + sendChord + '+Enter to send · Shift+Enter for newline · Up/Down for prompt history · Markdown toolbar supports ' + sendChord + '+B/I/K · paste images to attach';
2587
+ hints.textContent = 'Enter to send (queues while busy) · ' + sendChord + '+Enter to send now · Shift+Enter for newline · Up/Down for history · ' + sendChord + '+B/I/K · paste images to attach';
1720
2588
 
1721
2589
  // Assemble — clear and rebuild. Sprint 2: only one chrome row now
1722
2590
  // (the merged header). Reclaims ~36px of vertical space.
1723
2591
  while (container.firstChild) container.removeChild(container.firstChild);
1724
2592
  container.appendChild(header);
1725
2593
  container.appendChild(messagesArea);
2594
+ container.appendChild(workbarSlot);
1726
2595
  container.appendChild(createComposerResizeHandle(id));
1727
2596
  container.appendChild(inputBar);
1728
2597
  container.appendChild(hints);
@@ -1742,18 +2611,16 @@ window.WalleSession = (function() {
1742
2611
  });
1743
2612
  }
1744
2613
 
1745
- // Render existing messages
1746
- for (var i = 0; i < ws.messages.length; i++) {
1747
- renderMessage(messagesArea, ws.messages[i]);
1748
- }
1749
- syncPromptMessageDomIndices(id);
1750
- updatePromptNav(id);
2614
+ renderWorkBar(id);
2615
+ renderQueuePreview(id);
1751
2616
  renderComposerAttachments(id);
1752
- syncComposerChrome(id, textarea);
1753
- scrollToBottom(messagesArea);
1754
- focusInput(id);
2617
+ restoreComposerState(id, initialComposerSnapshot);
2618
+ updateSendButton(id, !!ws.isGenerating);
2619
+ if (!initialComposerSnapshot || !initialComposerSnapshot.focused) focusInput(id);
2620
+ renderMessagesIncrementally(id, ws, messagesArea, textarea);
1755
2621
 
1756
2622
  loadWalleModels().then(function() { syncWalleModelButtons(id); }).catch(function() {});
2623
+ ensureWalleBranchSnapshotLoaded(id);
1757
2624
  }
1758
2625
 
1759
2626
  // ---------- date divider ----------
@@ -1778,18 +2645,169 @@ window.WalleSession = (function() {
1778
2645
  container.appendChild(divider);
1779
2646
  }
1780
2647
 
2648
+ function renderBranchNav(id, messageIndex) {
2649
+ var ws = getState(id);
2650
+ var branches = ws && ws.branches ? ws.branches[String(messageIndex)] : null;
2651
+ if (!Array.isArray(branches) || branches.length <= 1) return null;
2652
+ var active = Number(ws.branchActive[String(messageIndex)] || 0);
2653
+ if (!Number.isFinite(active) || active < 0 || active >= branches.length) active = 0;
2654
+ var nav = document.createElement('span');
2655
+ nav.className = 'walle-branch-nav';
2656
+
2657
+ var prev = document.createElement('button');
2658
+ prev.type = 'button';
2659
+ prev.className = 'walle-branch-btn';
2660
+ prev.textContent = '<';
2661
+ prev.title = 'Previous prompt version';
2662
+ prev.setAttribute('aria-label', 'Previous prompt version');
2663
+ prev.disabled = active <= 0;
2664
+ prev.addEventListener('click', function(e) {
2665
+ e.preventDefault();
2666
+ e.stopPropagation();
2667
+ switchBranch(id, messageIndex, -1);
2668
+ });
2669
+ nav.appendChild(prev);
2670
+
2671
+ var label = document.createElement('span');
2672
+ label.className = 'walle-branch-label';
2673
+ label.textContent = (active + 1) + '/' + branches.length;
2674
+ label.title = 'Prompt versions';
2675
+ nav.appendChild(label);
2676
+
2677
+ var next = document.createElement('button');
2678
+ next.type = 'button';
2679
+ next.className = 'walle-branch-btn';
2680
+ next.textContent = '>';
2681
+ next.title = 'Next prompt version';
2682
+ next.setAttribute('aria-label', 'Next prompt version');
2683
+ next.disabled = active >= branches.length - 1;
2684
+ next.addEventListener('click', function(e) {
2685
+ e.preventDefault();
2686
+ e.stopPropagation();
2687
+ switchBranch(id, messageIndex, 1);
2688
+ });
2689
+ nav.appendChild(next);
2690
+ return nav;
2691
+ }
2692
+
2693
+ function renderUserMessageActions(id, messageIndex) {
2694
+ var wrap = document.createElement('span');
2695
+ wrap.className = 'walle-msg-actions';
2696
+ var branchNav = renderBranchNav(id, messageIndex);
2697
+ if (branchNav) wrap.appendChild(branchNav);
2698
+
2699
+ var edit = document.createElement('button');
2700
+ edit.type = 'button';
2701
+ edit.className = 'walle-msg-action';
2702
+ edit.textContent = 'Edit';
2703
+ edit.title = 'Edit and resend this prompt';
2704
+ edit.setAttribute('aria-label', 'Edit and resend prompt');
2705
+ edit.addEventListener('click', function(e) {
2706
+ e.preventDefault();
2707
+ e.stopPropagation();
2708
+ editMessage(id, messageIndex);
2709
+ });
2710
+ wrap.appendChild(edit);
2711
+
2712
+ var del = document.createElement('button');
2713
+ del.type = 'button';
2714
+ del.className = 'walle-msg-action danger';
2715
+ del.textContent = 'Delete';
2716
+ del.title = 'Delete this prompt and everything after it';
2717
+ del.setAttribute('aria-label', 'Delete prompt and truncate history');
2718
+ del.addEventListener('click', function(e) {
2719
+ e.preventDefault();
2720
+ e.stopPropagation();
2721
+ deleteFromPrompt(id, messageIndex);
2722
+ });
2723
+ wrap.appendChild(del);
2724
+ return wrap;
2725
+ }
2726
+
2727
+ function renderUserEditForm(id, messageIndex, msg) {
2728
+ var wrap = document.createElement('div');
2729
+ wrap.className = 'walle-edit-card';
2730
+
2731
+ var textarea = document.createElement('textarea');
2732
+ textarea.className = 'walle-edit-input';
2733
+ textarea.value = msg.content || '';
2734
+ textarea.rows = Math.min(10, Math.max(3, String(textarea.value || '').split('\n').length + 1));
2735
+ textarea.addEventListener('keydown', function(e) {
2736
+ if (e.key === 'Escape') {
2737
+ e.preventDefault();
2738
+ cancelEdit(id);
2739
+ } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
2740
+ e.preventDefault();
2741
+ submitEdit(id, messageIndex);
2742
+ }
2743
+ });
2744
+ wrap.appendChild(textarea);
2745
+
2746
+ var hint = document.createElement('div');
2747
+ hint.className = 'walle-edit-hint';
2748
+ hint.textContent = 'Resending creates a new version. Later messages stay available in the version switcher.';
2749
+ wrap.appendChild(hint);
2750
+
2751
+ var actions = document.createElement('div');
2752
+ actions.className = 'walle-edit-actions';
2753
+ var sendBtn = document.createElement('button');
2754
+ sendBtn.type = 'button';
2755
+ sendBtn.className = 'walle-session-action-btn primary';
2756
+ sendBtn.textContent = 'Resend';
2757
+ sendBtn.addEventListener('click', function(e) {
2758
+ e.preventDefault();
2759
+ submitEdit(id, messageIndex);
2760
+ });
2761
+ actions.appendChild(sendBtn);
2762
+
2763
+ var cancelBtn = document.createElement('button');
2764
+ cancelBtn.type = 'button';
2765
+ cancelBtn.className = 'walle-session-action-btn';
2766
+ cancelBtn.textContent = 'Cancel';
2767
+ cancelBtn.addEventListener('click', function(e) {
2768
+ e.preventDefault();
2769
+ cancelEdit(id);
2770
+ });
2771
+ actions.appendChild(cancelBtn);
2772
+ wrap.appendChild(actions);
2773
+ return wrap;
2774
+ }
2775
+
1781
2776
  // ---------- renderMessage ----------
1782
- function renderMessage(container, msg) {
2777
+ function renderMessage(container, msg, messageIndex) {
1783
2778
  var sessionId = '';
1784
2779
  if (container && container.id && container.id.indexOf('walle-messages-') === 0) {
1785
2780
  sessionId = container.id.slice('walle-messages-'.length);
1786
2781
  }
1787
- // Sprint 3: any time a real message lands, drop the empty-state chips.
1788
- _removeEmptyStateChips(container);
2782
+ if (msg && msg.liveActivity) {
2783
+ var livePanel = renderLiveActivity(msg.toolCalls || []);
2784
+ if (livePanel) {
2785
+ _removeEmptyAndLoadingState(container);
2786
+ _maybeAppendDateDivider(container, msg);
2787
+ livePanel.dataset.walleRole = msg.role || 'system';
2788
+ if (typeof messageIndex === 'number') livePanel.dataset.walleMessageIndex = String(messageIndex);
2789
+ container.appendChild(livePanel);
2790
+ return;
2791
+ }
2792
+ }
2793
+ // Durable approval card (persisted permission_request part): render it as the
2794
+ // same card the live stream shows, so a reload keeps a pending approval
2795
+ // actionable (and an answered one shown as resolved).
2796
+ if (msg && msg.metadata && msg.metadata.permission && msg.metadata.permission.permId) {
2797
+ _removeEmptyAndLoadingState(container);
2798
+ _maybeAppendDateDivider(container, msg);
2799
+ var histPermCard = renderPermissionCardForHistory(sessionId, msg.metadata.permission);
2800
+ if (typeof messageIndex === 'number') histPermCard.dataset.walleMessageIndex = String(messageIndex);
2801
+ container.appendChild(histPermCard);
2802
+ return;
2803
+ }
2804
+ // Sprint 3: any time a real message lands, drop the empty/loading state.
2805
+ _removeEmptyAndLoadingState(container);
1789
2806
  _maybeAppendDateDivider(container, msg);
1790
2807
  var el = document.createElement('div');
1791
2808
  el.className = 'walle-msg';
1792
2809
  el.dataset.walleRole = msg.role || '';
2810
+ if (typeof messageIndex === 'number') el.dataset.walleMessageIndex = String(messageIndex);
1793
2811
 
1794
2812
  var avatar = document.createElement('div');
1795
2813
  avatar.className = 'walle-msg-avatar ' + (msg.role === 'user' ? 'user' : 'assistant');
@@ -1839,6 +2857,10 @@ window.WalleSession = (function() {
1839
2857
  hdr.appendChild(lat);
1840
2858
  }
1841
2859
 
2860
+ if (msg.role === 'user' && sessionId && typeof messageIndex === 'number') {
2861
+ hdr.appendChild(renderUserMessageActions(sessionId, messageIndex));
2862
+ }
2863
+
1842
2864
  body.appendChild(hdr);
1843
2865
 
1844
2866
  // Tool activity (before body text). While a response is running, progress
@@ -1854,6 +2876,14 @@ window.WalleSession = (function() {
1854
2876
  body.appendChild(renderAttachmentStrip(sessionId, msg.attachments, false));
1855
2877
  }
1856
2878
 
2879
+ var wsForEdit = sessionId ? getState(sessionId) : null;
2880
+ if (msg.role === 'user' && wsForEdit && wsForEdit.editingMessageIndex === messageIndex) {
2881
+ body.appendChild(renderUserEditForm(sessionId, messageIndex, msg));
2882
+ el.appendChild(body);
2883
+ container.appendChild(el);
2884
+ return;
2885
+ }
2886
+
1857
2887
  // Message body — rendered as sanitized markdown
1858
2888
  var content = document.createElement('div');
1859
2889
  content.className = 'walle-msg-body';
@@ -1880,10 +2910,14 @@ window.WalleSession = (function() {
1880
2910
  card.className = 'walle-tool-card';
1881
2911
  card.dataset.expanded = 'false';
1882
2912
 
1883
- var header = document.createElement('div');
2913
+ var header = document.createElement('button');
2914
+ header.type = 'button';
1884
2915
  header.className = 'walle-tool-header';
2916
+ header.setAttribute('aria-expanded', 'false');
1885
2917
  header.onclick = function() {
1886
- card.dataset.expanded = card.dataset.expanded === 'true' ? 'false' : 'true';
2918
+ var expanded = card.dataset.expanded !== 'true';
2919
+ card.dataset.expanded = expanded ? 'true' : 'false';
2920
+ header.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1887
2921
  };
1888
2922
 
1889
2923
  var arrow = document.createElement('span');
@@ -1930,10 +2964,7 @@ window.WalleSession = (function() {
1930
2964
 
1931
2965
  card.appendChild(header);
1932
2966
 
1933
- var output = document.createElement('div');
1934
- output.className = 'walle-tool-output';
1935
- output.textContent = _formatToolOutput(tc.output);
1936
- card.appendChild(output);
2967
+ card.appendChild(renderToolDetail(tc));
1937
2968
 
1938
2969
  return card;
1939
2970
  }
@@ -1977,6 +3008,162 @@ window.WalleSession = (function() {
1977
3008
  return '';
1978
3009
  }
1979
3010
 
3011
+ function toolPrimaryLabel(toolName) {
3012
+ var name = String(toolName || '').toLowerCase();
3013
+ if (name === 'run_shell' || name === 'shell' || name === 'bash') return 'Command';
3014
+ if (name === 'browser_screenshot') return 'URL';
3015
+ if (name === 'read_file' || name === 'read' || name === 'cat') return 'Path';
3016
+ if (name === 'glob') return 'Pattern';
3017
+ if (name === 'grep_files' || name === 'grep') return 'Search';
3018
+ if (name === 'think') return 'Thought';
3019
+ return 'Input';
3020
+ }
3021
+
3022
+ function fullScalarText(value) {
3023
+ if (value == null) return '';
3024
+ if (typeof value === 'string') return value;
3025
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
3026
+ return '';
3027
+ }
3028
+
3029
+ function toolPrimaryText(tc) {
3030
+ if (!tc) return '';
3031
+ var toolName = String(tc.name || tc.tool || '').toLowerCase();
3032
+ var args = tc.args != null ? tc.args : tc.input;
3033
+ if (args == null) return '';
3034
+ if (typeof args === 'string' || typeof args === 'number' || typeof args === 'boolean') return String(args);
3035
+ if (typeof args !== 'object') return '';
3036
+ try {
3037
+ if (toolName === 'run_shell' || toolName === 'shell' || toolName === 'bash') {
3038
+ if (args.command) return String(args.command);
3039
+ if (Array.isArray(args.cmd)) return args.cmd.join(' ');
3040
+ }
3041
+ if (toolName === 'browser_screenshot') {
3042
+ return fullScalarText(args.url || args.href || args.target);
3043
+ }
3044
+ if (toolName === 'read_file' || toolName === 'read' || toolName === 'cat') {
3045
+ return fullScalarText(args.path || args.file_path);
3046
+ }
3047
+ if (toolName === 'glob') {
3048
+ return fullScalarText(args.pattern || args.path);
3049
+ }
3050
+ if (toolName === 'grep_files' || toolName === 'grep') {
3051
+ var p = fullScalarText(args.pattern || args.query);
3052
+ var loc = fullScalarText(args.path || args.glob);
3053
+ if (p && loc) return '"' + p + '" in ' + loc;
3054
+ return p || loc;
3055
+ }
3056
+ if (toolName === 'edit_file' || toolName === 'write_file' || toolName === 'edit' || toolName === 'write') {
3057
+ return fullScalarText(args.path || args.file_path);
3058
+ }
3059
+ if (toolName === 'list_directory' || toolName === 'ls') {
3060
+ return fullScalarText(args.path);
3061
+ }
3062
+ if (toolName === 'think') {
3063
+ return fullScalarText(args.thought || args.text);
3064
+ }
3065
+ if (toolName === 'search_memories') {
3066
+ return fullScalarText(args.query);
3067
+ }
3068
+ } catch (_) {
3069
+ return '';
3070
+ }
3071
+ return '';
3072
+ }
3073
+
3074
+ function formatToolArgs(args) {
3075
+ if (args == null) return '';
3076
+ if (typeof args === 'string') return args;
3077
+ if (typeof args === 'number' || typeof args === 'boolean') return String(args);
3078
+ try {
3079
+ return JSON.stringify(args, null, 2);
3080
+ } catch (_) {
3081
+ return String(args || '');
3082
+ }
3083
+ }
3084
+
3085
+ function appendToolDetailSection(parent, labelText, value, opts) {
3086
+ var text = value == null ? '' : String(value);
3087
+ if (!text.trim()) return null;
3088
+ opts = opts || {};
3089
+ var section = document.createElement('div');
3090
+ section.className = 'walle-tool-detail-section';
3091
+
3092
+ var label = document.createElement('div');
3093
+ label.className = 'walle-tool-detail-label';
3094
+ label.textContent = labelText;
3095
+ section.appendChild(label);
3096
+
3097
+ var pre = document.createElement('pre');
3098
+ pre.className = 'walle-tool-detail-pre' + (opts.output ? ' output' : '');
3099
+ pre.textContent = opts.output ? _formatToolOutput(text) : text;
3100
+ section.appendChild(pre);
3101
+
3102
+ parent.appendChild(section);
3103
+ return section;
3104
+ }
3105
+
3106
+ function renderToolDetail(tc) {
3107
+ var detail = document.createElement('div');
3108
+ detail.className = 'walle-tool-detail';
3109
+
3110
+ var meta = document.createElement('div');
3111
+ meta.className = 'walle-tool-detail-meta';
3112
+
3113
+ var statusChip = document.createElement('span');
3114
+ statusChip.className = 'walle-tool-detail-chip ' + (tc && tc.status ? tc.status : 'working');
3115
+ statusChip.textContent = tc && tc.status === 'error'
3116
+ ? 'Error'
3117
+ : (tc && tc.status === 'done' ? 'Done' : 'Running');
3118
+ meta.appendChild(statusChip);
3119
+
3120
+ var name = tc && (tc.name || tc.tool) ? displayToolName(tc.name || tc.tool) : '';
3121
+ if (name) {
3122
+ var nameChip = document.createElement('span');
3123
+ nameChip.className = 'walle-tool-detail-chip name';
3124
+ nameChip.textContent = name;
3125
+ meta.appendChild(nameChip);
3126
+ }
3127
+
3128
+ var duration = _extractDurationLabel(tc);
3129
+ if (duration) {
3130
+ var durationChip = document.createElement('span');
3131
+ durationChip.className = 'walle-tool-detail-chip';
3132
+ durationChip.textContent = duration;
3133
+ meta.appendChild(durationChip);
3134
+ }
3135
+ detail.appendChild(meta);
3136
+
3137
+ var summaryText = (tc && (tc.summary || tc.result_summary)) || '';
3138
+ if (summaryText) {
3139
+ var summary = document.createElement('div');
3140
+ summary.className = 'walle-tool-detail-summary';
3141
+ summary.textContent = summaryText;
3142
+ detail.appendChild(summary);
3143
+ }
3144
+
3145
+ var toolName = tc && (tc.name || tc.tool) || '';
3146
+ var primary = toolPrimaryText(tc);
3147
+ appendToolDetailSection(detail, toolPrimaryLabel(toolName), primary);
3148
+
3149
+ var args = tc ? (tc.args != null ? tc.args : tc.input) : null;
3150
+ var argsText = formatToolArgs(args);
3151
+ if (argsText && argsText !== primary) appendToolDetailSection(detail, 'Arguments', argsText);
3152
+
3153
+ var output = tc && (tc.output || tc.result || tc.error);
3154
+ appendToolDetailSection(detail, tc && tc.status === 'error' ? 'Error' : 'Output', output, { output: true });
3155
+
3156
+ if (!detail.querySelector('.walle-tool-detail-section') && !summaryText) {
3157
+ var empty = document.createElement('div');
3158
+ empty.className = 'walle-tool-detail-empty';
3159
+ empty.textContent = 'No detailed payload was recorded for this activity.';
3160
+ detail.appendChild(empty);
3161
+ }
3162
+
3163
+ addCopyButtonsToBlocks(detail);
3164
+ return detail;
3165
+ }
3166
+
1980
3167
  function isRenderableToolCall(tc) {
1981
3168
  if (!tc) return false;
1982
3169
  var toolName = tc.name || tc.tool || '';
@@ -2041,43 +3228,75 @@ window.WalleSession = (function() {
2041
3228
  var maxRows = 6;
2042
3229
  var start = Math.max(0, visible.length - maxRows);
2043
3230
  if (start > 0) {
2044
- var more = document.createElement('div');
3231
+ var more = document.createElement('button');
3232
+ more.type = 'button';
2045
3233
  more.className = 'walle-live-activity-more';
2046
- more.textContent = '+' + start + ' earlier';
3234
+ more.textContent = '+' + start + ' earlier · Show';
2047
3235
  list.appendChild(more);
3236
+
3237
+ var hiddenList = document.createElement('div');
3238
+ hiddenList.className = 'walle-live-activity-hidden';
3239
+ hiddenList.hidden = true;
3240
+ for (var h = 0; h < start; h++) hiddenList.appendChild(renderLiveActivityItem(visible[h]));
3241
+ more.onclick = function() {
3242
+ var showing = hiddenList.hidden;
3243
+ hiddenList.hidden = !showing;
3244
+ more.textContent = showing ? 'Hide earlier' : '+' + start + ' earlier · Show';
3245
+ };
3246
+ list.appendChild(hiddenList);
2048
3247
  }
2049
3248
  for (var i = start; i < visible.length; i++) {
2050
- var tc = visible[i];
2051
- var status = tc.status || 'working';
2052
- var row = document.createElement('div');
2053
- row.className = 'walle-live-activity-row ' + status;
3249
+ list.appendChild(renderLiveActivityItem(visible[i]));
3250
+ }
3251
+ panel.appendChild(list);
3252
+ return panel;
3253
+ }
2054
3254
 
2055
- var dot = document.createElement('span');
2056
- dot.className = 'walle-live-activity-dot';
2057
- row.appendChild(dot);
3255
+ function renderLiveActivityItem(tc) {
3256
+ var status = (tc && tc.status) || 'working';
3257
+ var item = document.createElement('details');
3258
+ item.className = 'walle-live-activity-item ' + status;
2058
3259
 
2059
- var main = document.createElement('div');
2060
- main.className = 'walle-live-activity-main';
3260
+ var row = document.createElement('summary');
3261
+ row.className = 'walle-live-activity-row ' + status;
3262
+ var label = displayToolName((tc && (tc.name || tc.tool)) || 'tool') || 'activity';
3263
+ var preview = toolPreviewText(tc);
3264
+ row.title = preview ? label + ': ' + preview : label;
3265
+ row.setAttribute('aria-label', 'Show details for ' + label + (preview ? ': ' + preview : ''));
2061
3266
 
2062
- var name = document.createElement('span');
2063
- name.className = 'walle-live-activity-name';
2064
- name.textContent = displayToolName(tc.name || tc.tool || 'tool') || 'activity';
2065
- main.appendChild(name);
2066
-
2067
- var text = document.createElement('span');
2068
- text.className = 'walle-live-activity-text';
2069
- text.textContent = toolPreviewText(tc);
2070
- main.appendChild(text);
2071
- row.appendChild(main);
3267
+ var arrow = document.createElement('span');
3268
+ arrow.className = 'walle-live-activity-arrow';
3269
+ arrow.textContent = '\u25B6';
3270
+ row.appendChild(arrow);
2072
3271
 
2073
- var meta = document.createElement('span');
2074
- meta.className = 'walle-live-activity-meta';
2075
- meta.textContent = toolStatusLabel(tc);
2076
- row.appendChild(meta);
2077
- list.appendChild(row);
2078
- }
2079
- panel.appendChild(list);
2080
- return panel;
3272
+ var dot = document.createElement('span');
3273
+ dot.className = 'walle-live-activity-dot';
3274
+ row.appendChild(dot);
3275
+
3276
+ var main = document.createElement('div');
3277
+ main.className = 'walle-live-activity-main';
3278
+
3279
+ var name = document.createElement('span');
3280
+ name.className = 'walle-live-activity-name';
3281
+ name.textContent = label;
3282
+ main.appendChild(name);
3283
+
3284
+ var text = document.createElement('span');
3285
+ text.className = 'walle-live-activity-text';
3286
+ text.textContent = preview;
3287
+ main.appendChild(text);
3288
+ row.appendChild(main);
3289
+
3290
+ var meta = document.createElement('span');
3291
+ meta.className = 'walle-live-activity-meta';
3292
+ meta.textContent = toolStatusLabel(tc);
3293
+ row.appendChild(meta);
3294
+ item.appendChild(row);
3295
+
3296
+ var detail = renderToolDetail(tc);
3297
+ detail.classList.add('walle-live-activity-detail');
3298
+ item.appendChild(detail);
3299
+ return item;
2081
3300
  }
2082
3301
 
2083
3302
  function normalizeToolCall(tc) {
@@ -2345,6 +3564,75 @@ window.WalleSession = (function() {
2345
3564
  }
2346
3565
 
2347
3566
  // ---------- permission card ----------
3567
+ // Editable working-directory control for the session header. Clicking the
3568
+ // path opens an inline input; committing sends `update-session-cwd` (the same
3569
+ // message the worktree-create flow uses), which updates session.cwd so the
3570
+ // next coding turn's effectiveCwd points there — no `cd <target>` prefix that
3571
+ // would trip the `cd * -> ask` permission rule on every command.
3572
+ function renderSessionCwdControl(sessionId, cwd) {
3573
+ var wrap = document.createElement('span');
3574
+ wrap.className = 'session-cwd';
3575
+ var current = cwd || '';
3576
+ var label = document.createElement('span');
3577
+ label.className = 'session-cwd-label' + (current ? '' : ' session-cwd-empty');
3578
+ label.textContent = current || 'Set working directory';
3579
+ label.title = "Click to change this session's working directory";
3580
+ label.tabIndex = 0;
3581
+ label.style.cursor = 'pointer';
3582
+ label.style.borderBottom = '1px dotted currentColor';
3583
+ wrap.appendChild(label);
3584
+
3585
+ function restore() {
3586
+ wrap.replaceChildren();
3587
+ label.textContent = current || 'Set working directory';
3588
+ label.className = 'session-cwd-label' + (current ? '' : ' session-cwd-empty');
3589
+ wrap.appendChild(label);
3590
+ }
3591
+ function commit(rawVal) {
3592
+ var val = (rawVal || '').trim();
3593
+ if (val && val !== current) {
3594
+ current = val;
3595
+ send({ type: 'update-session-cwd', id: sessionId, cwd: val });
3596
+ }
3597
+ restore();
3598
+ }
3599
+ function beginEdit() {
3600
+ var input = document.createElement('input');
3601
+ input.type = 'text';
3602
+ input.className = 'session-cwd-input';
3603
+ input.value = current;
3604
+ input.placeholder = '/absolute/path/to/project';
3605
+ input.style.minWidth = '220px';
3606
+ // Fit the dark session-meta row rather than a default white box.
3607
+ input.style.background = 'rgba(255,255,255,0.06)';
3608
+ input.style.color = 'inherit';
3609
+ input.style.border = '1px solid rgba(255,255,255,0.25)';
3610
+ input.style.borderRadius = '4px';
3611
+ input.style.padding = '1px 5px';
3612
+ input.style.font = 'inherit';
3613
+ wrap.replaceChildren();
3614
+ wrap.appendChild(input);
3615
+ input.focus();
3616
+ input.select();
3617
+ var done = false;
3618
+ var finish = function(save) {
3619
+ if (done) return;
3620
+ done = true;
3621
+ if (save) commit(input.value); else restore();
3622
+ };
3623
+ input.addEventListener('keydown', function(e) {
3624
+ if (e.key === 'Enter') { e.preventDefault(); finish(true); }
3625
+ else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
3626
+ });
3627
+ input.addEventListener('blur', function() { finish(true); });
3628
+ }
3629
+ label.addEventListener('click', beginEdit);
3630
+ label.addEventListener('keydown', function(e) {
3631
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); beginEdit(); }
3632
+ });
3633
+ return wrap;
3634
+ }
3635
+
2348
3636
  function renderPermissionCard(sessionId, ev) {
2349
3637
  var card = document.createElement('div');
2350
3638
  card.className = 'walle-perm-card';
@@ -2380,15 +3668,7 @@ window.WalleSession = (function() {
2380
3668
  permId: ev.permId,
2381
3669
  decision: decision,
2382
3670
  });
2383
- // Mark card as resolved
2384
- card.classList.add('resolved');
2385
- if (decision === 'deny') {
2386
- card.classList.add('perm-denied');
2387
- card.setAttribute('data-resolved', 'Denied');
2388
- } else {
2389
- card.classList.add('perm-approved');
2390
- card.setAttribute('data-resolved', decision === 'allow_always' ? 'Allowed (always)' : 'Allowed (once)');
2391
- }
3671
+ markPermissionCardResolved(card, decision === 'deny' ? 'deny' : decision, '');
2392
3672
  };
2393
3673
  })(buttons[i]));
2394
3674
  }
@@ -2396,6 +3676,38 @@ window.WalleSession = (function() {
2396
3676
  return card;
2397
3677
  }
2398
3678
 
3679
+ // Apply the resolved visual state to a permission card. `decision` is the
3680
+ // card's button decision (allow_once/allow_always/deny) or a backend decision
3681
+ // (allow/deny); `label` overrides the badge text when provided.
3682
+ function markPermissionCardResolved(card, decision, label) {
3683
+ if (!card || card.classList.contains('resolved')) return;
3684
+ card.classList.add('resolved');
3685
+ var denied = decision === 'deny' || decision === 'reject';
3686
+ if (denied) {
3687
+ card.classList.add('perm-denied');
3688
+ card.setAttribute('data-resolved', label || 'Denied');
3689
+ } else {
3690
+ card.classList.add('perm-approved');
3691
+ var allowedLabel = label || (decision === 'allow_always' || decision === 'always'
3692
+ ? 'Allowed (always)'
3693
+ : 'Allowed (once)');
3694
+ card.setAttribute('data-resolved', allowedLabel);
3695
+ }
3696
+ var btns = card.querySelectorAll('.walle-perm-actions button');
3697
+ for (var b = 0; b < btns.length; b++) btns[b].disabled = true;
3698
+ }
3699
+
3700
+ // Render a permission card from persisted history. Pending cards stay
3701
+ // actionable (reply re-routes to the same handler); resolved cards render in
3702
+ // their final state with the decision badge and disabled buttons.
3703
+ function renderPermissionCardForHistory(sessionId, permission) {
3704
+ var card = renderPermissionCard(sessionId, permission);
3705
+ if (permission && permission.status === 'resolved') {
3706
+ markPermissionCardResolved(card, permission.decision || 'allow', '');
3707
+ }
3708
+ return card;
3709
+ }
3710
+
2399
3711
  // ---------- sendMessage ----------
2400
3712
  function resolveContextSessionId(id) {
2401
3713
  var preferred = state.lastActiveWorkSessionId || state._savedActiveSession || '';
@@ -2428,10 +3740,12 @@ window.WalleSession = (function() {
2428
3740
  ws.messages.push(msg);
2429
3741
  ws.messageCount++;
2430
3742
  recordLiveRoleDelivery(ws, 'user');
3743
+ if (hasBranchSnapshot(ws)) saveWalleBranchSnapshot(id);
2431
3744
 
2432
3745
  var messagesArea = document.getElementById('walle-messages-' + id);
2433
3746
  if (messagesArea) {
2434
- renderMessage(messagesArea, msg);
3747
+ if (restartVisibleMessageRenderIfBusy(id, ws)) return msg;
3748
+ renderMessage(messagesArea, msg, ws.messages.length - 1);
2435
3749
  syncPromptMessageDomIndices(id);
2436
3750
  updatePromptNav(id);
2437
3751
  scrollToBottom(messagesArea);
@@ -2439,6 +3753,145 @@ window.WalleSession = (function() {
2439
3753
  return msg;
2440
3754
  }
2441
3755
 
3756
+ function rerenderWalleSessionPreservingComposer(id, focusSelector) {
3757
+ var composerSnapshot = captureComposerState(id);
3758
+ renderSession(id);
3759
+ restoreComposerState(id, composerSnapshot);
3760
+ if (focusSelector) {
3761
+ setTimeout(function() {
3762
+ var root = document.getElementById('walle-session-' + id);
3763
+ var target = root && root.querySelector(focusSelector);
3764
+ if (target && typeof target.focus === 'function') {
3765
+ target.focus();
3766
+ if (target.setSelectionRange) {
3767
+ var len = target.value ? target.value.length : 0;
3768
+ target.setSelectionRange(len, len);
3769
+ }
3770
+ }
3771
+ }, 0);
3772
+ }
3773
+ }
3774
+
3775
+ function editMessage(id, messageIndex) {
3776
+ var ws = getState(id);
3777
+ if (!ws) return;
3778
+ if (ws.isGenerating) {
3779
+ if (typeof toast === 'function') toast('Stop Wall-E before editing history.', { type: 'warning', duration: 2500 });
3780
+ return;
3781
+ }
3782
+ var msg = ws.messages[messageIndex];
3783
+ if (!msg || msg.role !== 'user') return;
3784
+ ws.editingMessageIndex = messageIndex;
3785
+ rerenderWalleSessionPreservingComposer(id, '.walle-edit-input');
3786
+ }
3787
+
3788
+ function cancelEdit(id) {
3789
+ var ws = getState(id);
3790
+ if (!ws) return;
3791
+ ws.editingMessageIndex = -1;
3792
+ rerenderWalleSessionPreservingComposer(id);
3793
+ }
3794
+
3795
+ function submitEdit(id, messageIndex) {
3796
+ var ws = getState(id);
3797
+ if (!ws) return;
3798
+ if (ws.isGenerating) {
3799
+ if (typeof toast === 'function') toast('Stop Wall-E before editing history.', { type: 'warning', duration: 2500 });
3800
+ return;
3801
+ }
3802
+ var root = document.getElementById('walle-session-' + id);
3803
+ var textarea = root && root.querySelector('.walle-edit-input');
3804
+ var text = String(textarea && textarea.value || '').trim();
3805
+ var original = ws.messages[messageIndex];
3806
+ if (!original || original.role !== 'user') return;
3807
+ var attachments = Array.isArray(original.attachments) ? cloneJsonSafe(original.attachments, []) : [];
3808
+ if (!text && !attachments.length) {
3809
+ if (typeof toast === 'function') toast('Edited prompt is empty.', { type: 'warning', duration: 2000 });
3810
+ return;
3811
+ }
3812
+
3813
+ var oldTail = ws.messages.slice(messageIndex).map(cloneBranchMessage);
3814
+ var key = String(messageIndex);
3815
+ var branches = Array.isArray(ws.branches[key]) ? ws.branches[key] : null;
3816
+ if (!branches) {
3817
+ branches = [oldTail];
3818
+ ws.branches[key] = branches;
3819
+ ws.branchActive[key] = 1;
3820
+ } else {
3821
+ var current = Number(ws.branchActive[key] || 0);
3822
+ if (!Number.isFinite(current) || current < 0 || current >= branches.length) current = 0;
3823
+ branches[current] = oldTail;
3824
+ ws.branchActive[key] = branches.length;
3825
+ }
3826
+
3827
+ var editedMessage = cloneBranchMessage(original);
3828
+ editedMessage.content = text;
3829
+ editedMessage.timestamp = Date.now();
3830
+ editedMessage.attachments = attachments;
3831
+ branches.push([editedMessage]);
3832
+ clearBranchesAfter(ws, messageIndex, false);
3833
+
3834
+ ws.messages = ws.messages.slice(0, messageIndex).map(cloneBranchMessage).concat([editedMessage]);
3835
+ ws.messageCount = ws.messages.length;
3836
+ ws.editingMessageIndex = -1;
3837
+ markBranchHistoryAuthoritative(ws);
3838
+ recordLiveRoleDelivery(ws, 'user');
3839
+ rerenderWalleSessionPreservingComposer(id);
3840
+ saveWalleBranchSnapshot(id);
3841
+
3842
+ sendOutboundMessage(id, text, attachments, {
3843
+ appendUser: false,
3844
+ recordHistory: true,
3845
+ outboundText: appendAttachmentReferences(text, attachments),
3846
+ timestamp: editedMessage.timestamp
3847
+ });
3848
+ }
3849
+
3850
+ function deleteFromPrompt(id, messageIndex) {
3851
+ var ws = getState(id);
3852
+ if (!ws) return;
3853
+ if (ws.isGenerating) {
3854
+ if (typeof toast === 'function') toast('Stop Wall-E before deleting history.', { type: 'warning', duration: 2500 });
3855
+ return;
3856
+ }
3857
+ var msg = ws.messages[messageIndex];
3858
+ if (!msg || msg.role !== 'user') return;
3859
+ var ok = window.confirm('Delete this prompt and all messages after it? This truncates the active conversation. Existing earlier prompt versions are kept.');
3860
+ if (!ok) return;
3861
+ ws.messages = ws.messages.slice(0, messageIndex).map(cloneBranchMessage);
3862
+ ws.messageCount = ws.messages.length;
3863
+ ws.editingMessageIndex = -1;
3864
+ clearBranchesAfter(ws, messageIndex, true);
3865
+ refreshActiveBranchSlots(ws);
3866
+ markBranchHistoryAuthoritative(ws);
3867
+ rerenderWalleSessionPreservingComposer(id);
3868
+ saveWalleBranchSnapshot(id);
3869
+ }
3870
+
3871
+ function switchBranch(id, messageIndex, direction) {
3872
+ var ws = getState(id);
3873
+ if (!ws || ws.isGenerating) {
3874
+ if (ws && ws.isGenerating && typeof toast === 'function') toast('Stop Wall-E before switching prompt versions.', { type: 'warning', duration: 2500 });
3875
+ return;
3876
+ }
3877
+ var key = String(messageIndex);
3878
+ var branches = ws.branches[key];
3879
+ if (!Array.isArray(branches) || branches.length <= 1) return;
3880
+ var current = Number(ws.branchActive[key] || 0);
3881
+ if (!Number.isFinite(current) || current < 0 || current >= branches.length) current = 0;
3882
+ var next = current + direction;
3883
+ if (next < 0 || next >= branches.length) return;
3884
+ branches[current] = ws.messages.slice(messageIndex).map(cloneBranchMessage);
3885
+ ws.branchActive[key] = next;
3886
+ ws.messages = ws.messages.slice(0, messageIndex).map(cloneBranchMessage).concat(normalizeBranchTail(branches[next]));
3887
+ ws.messageCount = ws.messages.length;
3888
+ ws.editingMessageIndex = -1;
3889
+ clearBranchesAfter(ws, messageIndex, false);
3890
+ markBranchHistoryAuthoritative(ws);
3891
+ rerenderWalleSessionPreservingComposer(id);
3892
+ saveWalleBranchSnapshot(id);
3893
+ }
3894
+
2442
3895
  function recordLiveRoleDelivery(ws, role) {
2443
3896
  if (!ws || !role) return;
2444
3897
  if (!ws._lastLiveRoleAt) ws._lastLiveRoleAt = {};
@@ -2467,6 +3920,27 @@ window.WalleSession = (function() {
2467
3920
  return false;
2468
3921
  }
2469
3922
 
3923
+ function isOpenTurnActivityMessage(msg) {
3924
+ if (!msg || !msg.liveActivity) return false;
3925
+ var meta = msg.metadata || {};
3926
+ if (meta.source === 'walle-open-turn-activity') return true;
3927
+ var content = String(msg.content || msg.text || '').trim();
3928
+ return msg.role === 'system' && content.indexOf('Wall-E is working on this prompt.') === 0;
3929
+ }
3930
+
3931
+ function captureLiveTurnState(id, ws) {
3932
+ if (!ws || !ws.isGenerating) return null;
3933
+ var messagesArea = document.getElementById('walle-messages-' + id);
3934
+ var hasLiveDom = !!(messagesArea && messagesArea.querySelector('.walle-live-activity, .walle-thinking'));
3935
+ var assistant = ws._currentAssistant || null;
3936
+ var hasAssistantActivity = !!(assistant && Array.isArray(assistant.toolCalls) && assistant.toolCalls.length);
3937
+ if (!hasOpenUserTurn(ws) && !hasLiveDom && !hasAssistantActivity) return null;
3938
+ return {
3939
+ currentAssistant: assistant,
3940
+ hadLiveDom: hasLiveDom || hasAssistantActivity
3941
+ };
3942
+ }
3943
+
2470
3944
  function ensurePendingThinking(id) {
2471
3945
  var ws = getState(id);
2472
3946
  var messagesArea = document.getElementById('walle-messages-' + id);
@@ -2487,8 +3961,7 @@ window.WalleSession = (function() {
2487
3961
  var latestSession = state.sessions.get(id);
2488
3962
  if (!latestSession || !walleSessionIsActive(id)) return;
2489
3963
  latestSession.needsAttach = false;
2490
- latestSession._walleHistoryRequested = true;
2491
- send({ type: 'attach', id: id, reason: reason || 'walle-transcript-append' });
3964
+ requestHistory(id, reason || 'walle-transcript-append', { force: true, noCache: true });
2492
3965
  }, TRANSCRIPT_ATTACH_DEBOUNCE_MS);
2493
3966
  }
2494
3967
 
@@ -2503,15 +3976,633 @@ window.WalleSession = (function() {
2503
3976
  return Promise.resolve();
2504
3977
  }
2505
3978
 
3979
+ var WORK_ACTIVE_STATUSES = { queued: true, running: true, stopping: true };
3980
+
3981
+ function normalizeWorkState(raw, id) {
3982
+ raw = raw && typeof raw === 'object' ? raw : {};
3983
+ var tasks = Array.isArray(raw.tasks) ? raw.tasks : [];
3984
+ var active = Array.isArray(raw.active) ? raw.active : tasks.filter(function(task) {
3985
+ return !!WORK_ACTIVE_STATUSES[String(task && task.status || '').toLowerCase()];
3986
+ });
3987
+ var recent = Array.isArray(raw.recent) ? raw.recent : tasks.filter(function(task) {
3988
+ return !WORK_ACTIVE_STATUSES[String(task && task.status || '').toLowerCase()];
3989
+ });
3990
+ var counts = raw.counts && typeof raw.counts === 'object' ? raw.counts : {};
3991
+ return {
3992
+ sessionId: raw.sessionId || raw.id || id,
3993
+ updatedAt: Number(raw.updatedAt || Date.now()),
3994
+ tasks: tasks,
3995
+ active: active,
3996
+ recent: recent,
3997
+ counts: {
3998
+ running: Number(counts.running || active.length || 0),
3999
+ shells: Number(counts.shells || 0),
4000
+ agents: Number(counts.agents || 0),
4001
+ tools: Number(counts.tools || 0),
4002
+ skills: Number(counts.skills || 0),
4003
+ queued: Number(counts.queued || 0)
4004
+ },
4005
+ isolation: raw.isolation || null
4006
+ };
4007
+ }
4008
+
4009
+ function workStateHasSignal(work) {
4010
+ if (!work) return false;
4011
+ var counts = work.counts || {};
4012
+ return (work.active && work.active.length)
4013
+ || (work.recent && work.recent.length)
4014
+ || Number(counts.queued || 0) > 0;
4015
+ }
4016
+
4017
+ function workKindLabel(kind) {
4018
+ var key = String(kind || '').toLowerCase();
4019
+ if (key === 'shell') return 'shell';
4020
+ if (key === 'agent') return 'agent';
4021
+ if (key === 'skill') return 'skill';
4022
+ if (key === 'turn') return 'turn';
4023
+ return 'tool';
4024
+ }
4025
+
4026
+ function workTaskAge(task) {
4027
+ var start = Number(task && (task.startedAt || task.started_at) || 0);
4028
+ if (!start) return '';
4029
+ var ms = Math.max(0, Date.now() - start);
4030
+ if (ms < 1000) return 'now';
4031
+ if (ms < 60000) return Math.floor(ms / 1000) + 's';
4032
+ if (ms < 3600000) return Math.floor(ms / 60000) + 'm';
4033
+ return Math.floor(ms / 3600000) + 'h';
4034
+ }
4035
+
4036
+ function workChip(label, value, tone) {
4037
+ var chip = document.createElement('span');
4038
+ chip.className = 'walle-work-chip' + (tone ? ' ' + tone : '');
4039
+ chip.textContent = value ? label + ' ' + value : label;
4040
+ return chip;
4041
+ }
4042
+
4043
+ function createWorkTaskRow(id, task) {
4044
+ task = task || {};
4045
+ var row = document.createElement('div');
4046
+ row.className = 'walle-work-row ' + String(task.status || 'running').toLowerCase();
4047
+
4048
+ var dot = document.createElement('span');
4049
+ dot.className = 'walle-work-dot';
4050
+ row.appendChild(dot);
4051
+
4052
+ var body = document.createElement('span');
4053
+ body.className = 'walle-work-row-body';
4054
+ var title = document.createElement('span');
4055
+ title.className = 'walle-work-row-title';
4056
+ title.textContent = task.label || task.summary || task.toolName || workKindLabel(task.kind);
4057
+ body.appendChild(title);
4058
+ var meta = document.createElement('span');
4059
+ meta.className = 'walle-work-row-meta';
4060
+ meta.textContent = [
4061
+ workKindLabel(task.kind),
4062
+ task.status || 'running',
4063
+ task.workerScope || task.worker_scope || '',
4064
+ workTaskAge(task)
4065
+ ].filter(Boolean).join(' · ');
4066
+ body.appendChild(meta);
4067
+ if (task.inputPreview || task.input_preview || task.summary) {
4068
+ var preview = document.createElement('span');
4069
+ preview.className = 'walle-work-row-preview';
4070
+ preview.textContent = task.inputPreview || task.input_preview || task.summary || '';
4071
+ body.appendChild(preview);
4072
+ }
4073
+ row.appendChild(body);
4074
+
4075
+ var actions = document.createElement('span');
4076
+ actions.className = 'walle-work-row-actions';
4077
+ var view = document.createElement('button');
4078
+ view.type = 'button';
4079
+ view.textContent = 'View';
4080
+ view.onclick = function() {
4081
+ var area = document.getElementById('walle-messages-' + id);
4082
+ var live = area && area.querySelector('.walle-live-activity, .walle-tool-group, .walle-thinking');
4083
+ (live || area)?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
4084
+ if (live && live.classList) live.classList.add('walle-work-focus');
4085
+ setTimeout(function() { if (live && live.classList) live.classList.remove('walle-work-focus'); }, 1200);
4086
+ };
4087
+ actions.appendChild(view);
4088
+ if (task.kind === 'turn' && WORK_ACTIVE_STATUSES[String(task.status || '').toLowerCase()]) {
4089
+ var stop = document.createElement('button');
4090
+ stop.type = 'button';
4091
+ stop.className = 'danger';
4092
+ stop.textContent = 'Stop';
4093
+ stop.onclick = function() { send({ type: 'walle-cancel', id: id }); };
4094
+ actions.appendChild(stop);
4095
+ }
4096
+ row.appendChild(actions);
4097
+ return row;
4098
+ }
4099
+
4100
+ function renderWorkDrawer(id, root, work) {
4101
+ var drawer = document.createElement('div');
4102
+ drawer.className = 'walle-work-drawer';
4103
+ var headline = document.createElement('div');
4104
+ headline.className = 'walle-work-drawer-head';
4105
+ var title = document.createElement('strong');
4106
+ title.textContent = 'Runtime work';
4107
+ headline.appendChild(title);
4108
+ var note = document.createElement('span');
4109
+ note.textContent = 'metadata only · output stays in worker/runtime logs';
4110
+ headline.appendChild(note);
4111
+ drawer.appendChild(headline);
4112
+
4113
+ var active = (work.active || []).slice(0, 16);
4114
+ var recent = (work.recent || []).slice(0, 8);
4115
+ var groups = [
4116
+ { label: 'Active', rows: active },
4117
+ { label: 'Recent', rows: recent }
4118
+ ];
4119
+ groups.forEach(function(group) {
4120
+ if (!group.rows.length) return;
4121
+ var groupEl = document.createElement('div');
4122
+ groupEl.className = 'walle-work-group';
4123
+ var groupTitle = document.createElement('div');
4124
+ groupTitle.className = 'walle-work-group-title';
4125
+ groupTitle.textContent = group.label;
4126
+ groupEl.appendChild(groupTitle);
4127
+ group.rows.forEach(function(task) { groupEl.appendChild(createWorkTaskRow(id, task)); });
4128
+ drawer.appendChild(groupEl);
4129
+ });
4130
+ if (!active.length && !recent.length) {
4131
+ var empty = document.createElement('div');
4132
+ empty.className = 'walle-work-empty';
4133
+ empty.textContent = 'No runtime work is active for this session.';
4134
+ drawer.appendChild(empty);
4135
+ }
4136
+ root.appendChild(drawer);
4137
+ }
4138
+
4139
+ function renderWorkBar(id) {
4140
+ var ws = getState(id);
4141
+ var slot = document.getElementById('walle-workbar-' + id);
4142
+ if (!ws || !slot) return;
4143
+ var work = normalizeWorkState(ws.workState, id);
4144
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
4145
+ if (!workStateHasSignal(work) && !ws.workDrawerOpen) {
4146
+ slot.style.display = 'none';
4147
+ return;
4148
+ }
4149
+ slot.style.display = 'block';
4150
+
4151
+ var root = document.createElement('div');
4152
+ root.className = 'walle-workbar' + (ws.workDrawerOpen ? ' expanded' : '');
4153
+
4154
+ var rail = document.createElement('div');
4155
+ rail.className = 'walle-workbar-rail';
4156
+ var kicker = document.createElement('span');
4157
+ kicker.className = 'walle-workbar-kicker';
4158
+ kicker.textContent = 'WORK';
4159
+ rail.appendChild(kicker);
4160
+
4161
+ var counts = work.counts || {};
4162
+ if (counts.running) rail.appendChild(workChip('running', counts.running, 'active'));
4163
+ if (counts.shells) rail.appendChild(workChip('shell', counts.shells, 'shell'));
4164
+ if (counts.agents) rail.appendChild(workChip('agent', counts.agents, 'agent'));
4165
+ if (counts.tools) rail.appendChild(workChip('tool', counts.tools, 'tool'));
4166
+ if (counts.skills) rail.appendChild(workChip('skill', counts.skills, 'skill'));
4167
+ // (queued count intentionally omitted — queued prompts now render inline as
4168
+ // pending bubbles + a status line above the composer, not as a work-bar chip)
4169
+ if (!counts.running && work.recent && work.recent.length) rail.appendChild(workChip('recent', work.recent.length, 'done'));
4170
+ rail.appendChild(workChip('isolated', '', 'isolated'));
4171
+ root.appendChild(rail);
4172
+
4173
+ var summary = document.createElement('div');
4174
+ summary.className = 'walle-workbar-summary';
4175
+ var focusTask = (work.active && work.active[0]) || (work.recent && work.recent[0]) || null;
4176
+ summary.textContent = focusTask
4177
+ ? (focusTask.summary || focusTask.label || 'Runtime work updated')
4178
+ : 'Runtime work is idle.';
4179
+ root.appendChild(summary);
4180
+
4181
+ var actions = document.createElement('div');
4182
+ actions.className = 'walle-workbar-actions';
4183
+ var manage = document.createElement('button');
4184
+ manage.type = 'button';
4185
+ manage.className = 'walle-workbar-manage';
4186
+ manage.textContent = ws.workDrawerOpen ? 'Hide' : 'Manage';
4187
+ manage.onclick = function() {
4188
+ var latest = getState(id);
4189
+ if (!latest) return;
4190
+ latest.workDrawerOpen = !latest.workDrawerOpen;
4191
+ renderWorkBar(id);
4192
+ };
4193
+ actions.appendChild(manage);
4194
+ if (work.active && work.active.some(function(task) { return task.kind === 'turn'; })) {
4195
+ var stop = document.createElement('button');
4196
+ stop.type = 'button';
4197
+ stop.className = 'walle-workbar-stop';
4198
+ stop.textContent = 'Stop';
4199
+ stop.onclick = function() { send({ type: 'walle-cancel', id: id }); };
4200
+ actions.appendChild(stop);
4201
+ }
4202
+ root.appendChild(actions);
4203
+ if (ws.workDrawerOpen) renderWorkDrawer(id, root, work);
4204
+ slot.appendChild(root);
4205
+ }
4206
+
4207
+ function confirmCancelActiveRun(id, event) {
4208
+ var ws = getState(id);
4209
+ if (!ws || !ws.isGenerating || _cancelConfirmationOpen) return false;
4210
+ if (event) {
4211
+ event.preventDefault();
4212
+ event.stopPropagation();
4213
+ }
4214
+ _cancelConfirmationOpen = true;
4215
+ var accepted = false;
4216
+ try {
4217
+ accepted = window.confirm('Stop the current Wall-E run?');
4218
+ } finally {
4219
+ _cancelConfirmationOpen = false;
4220
+ }
4221
+ if (!accepted) return true;
4222
+ send({ type: 'walle-cancel', id: id });
4223
+ return true;
4224
+ }
4225
+
4226
+ function hasEscapeDismissibleWalleUi() {
4227
+ var picker = document.getElementById('slash-picker');
4228
+ if (picker && picker.style && picker.style.display !== 'none') return true;
4229
+ if (_walleModelPicker && _walleModelPicker.el && document.body.contains(_walleModelPicker.el)) return true;
4230
+ return false;
4231
+ }
4232
+
4233
+ function handleGlobalEscape(id, event) {
4234
+ if (!id || !event || event.defaultPrevented) return false;
4235
+ if (event.key !== 'Escape' || event.metaKey || event.ctrlKey || event.altKey) return false;
4236
+ if (hasEscapeDismissibleWalleUi()) return false;
4237
+ return confirmCancelActiveRun(id, event);
4238
+ }
4239
+
4240
+ function handleWorkState(msg) {
4241
+ var id = msg && (msg.sessionId || msg.id);
4242
+ if (!id) return;
4243
+ var ws = getState(id);
4244
+ if (!ws) return;
4245
+ ws.workState = normalizeWorkState(msg.state || msg.workState || msg, id);
4246
+ renderWorkBar(id);
4247
+ }
4248
+
4249
+ function queueApiUrl(path) {
4250
+ return path;
4251
+ }
4252
+
4253
+ function queueItemIsActive(item) {
4254
+ var status = String(item && item.status || 'pending').toLowerCase();
4255
+ return status !== 'sent' && status !== 'skipped';
4256
+ }
4257
+
4258
+ function activeQueueItems(queue) {
4259
+ var items = queue && Array.isArray(queue.items) ? queue.items : [];
4260
+ return items.filter(queueItemIsActive);
4261
+ }
4262
+
4263
+ function selectedQueueItem(queue) {
4264
+ if (!queue) return null;
4265
+ var items = queue.items || [];
4266
+ var current = items[queue.currentIndex];
4267
+ if (queueItemIsActive(current)) return current;
4268
+ var active = activeQueueItems(queue);
4269
+ return active.length ? active[0] : null;
4270
+ }
4271
+
4272
+ function queueTitleForText(text) {
4273
+ var compact = String(text || '').replace(/\s+/g, ' ').trim();
4274
+ return compact ? compact.slice(0, 88) : 'Queued prompt';
4275
+ }
4276
+
4277
+ function queueStateRevisionValue(value) {
4278
+ var raw = Number(value && value.revision);
4279
+ return Number.isFinite(raw) ? raw : null;
4280
+ }
4281
+
4282
+ function shouldApplyQueueState(ws, msg) {
4283
+ if (!ws || !msg || !msg.sessionId) return true;
4284
+ var current = ws.queueState;
4285
+ if (!current || current.sessionId !== msg.sessionId) return true;
4286
+ var nextRevision = queueStateRevisionValue(msg);
4287
+ var currentRevision = queueStateRevisionValue(current);
4288
+ if (nextRevision === null || currentRevision === null) return true;
4289
+ return nextRevision >= currentRevision;
4290
+ }
4291
+
4292
+ function normalizeQueueImage(att, idx) {
4293
+ att = att || {};
4294
+ return {
4295
+ type: att.type || 'image',
4296
+ name: att.name || att.filename || ('image-' + (idx + 1) + '.png'),
4297
+ filename: att.filename || att.name || '',
4298
+ data: att.data || '',
4299
+ label: att.label || ('[Image #' + (idx + 1) + ']'),
4300
+ url: att.url || '',
4301
+ path: att.path || att.file_path || '',
4302
+ mediaType: att.mediaType || att.mimeType || '',
4303
+ imageWidth: att.imageWidth || null,
4304
+ imageHeight: att.imageHeight || null,
4305
+ originalWidth: att.originalWidth || null,
4306
+ originalHeight: att.originalHeight || null,
4307
+ resizedForProvider: !!att.resizedForProvider
4308
+ };
4309
+ }
4310
+
4311
+ // POST a queue action (next/mode/remove…) and refresh the inline UI.
4312
+ function queuePostAction(id, action, payload) {
4313
+ var ws = getState(id);
4314
+ return fetch(queueApiUrl('/api/queues/' + encodeURIComponent(id) + '/' + action), {
4315
+ method: 'POST',
4316
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + ((state && state.token) || '') },
4317
+ body: payload ? JSON.stringify(payload) : undefined,
4318
+ }).then(function(r) { return r.ok ? r.json() : null; })
4319
+ .then(function(data) { if (data && ws) { ws.queueState = data; renderQueuePreview(id); renderPendingQueueBubbles(id); } })
4320
+ .catch(function() {});
4321
+ }
4322
+
4323
+ function removeQueuedItem(id, itemId, activeCount) {
4324
+ // Removing the last active item == cancel the whole queue (clears state cleanly).
4325
+ if (activeCount <= 1) { cancelQueuedMessages(id); return; }
4326
+ queuePostAction(id, 'remove', { itemId: itemId });
4327
+ }
4328
+
4329
+ // Edit a queued prompt: pull its text + images back into the composer and drop
4330
+ // it from the queue, so the user re-sends after tweaking.
4331
+ function editQueuedItem(id, item, activeCount) {
4332
+ var ws = getState(id);
4333
+ if (!ws) return;
4334
+ var container = document.getElementById('walle-session-' + id);
4335
+ var textarea = container && container.querySelector('.walle-input');
4336
+ if (textarea) {
4337
+ textarea.value = String(item.text || '');
4338
+ if (typeof syncComposerChrome === 'function') syncComposerChrome(id, textarea);
4339
+ }
4340
+ ws.inputAttachments = (Array.isArray(item.images) ? item.images : []).map(function(im, i) { return normalizeQueueImage(im, i); });
4341
+ renderComposerAttachments(id);
4342
+ removeQueuedItem(id, item.id, activeCount);
4343
+ focusInput(id);
4344
+ }
4345
+
4346
+ // Render queued prompts INLINE in the conversation as faded "You · queued"
4347
+ // bubbles pinned to the bottom of the message list (CSS order keeps them last
4348
+ // even while Wall-E streams). This is where the user typed them, so they read
4349
+ // as the natural continuation of the conversation — not a separate banner.
4350
+ // Standard HTML5 drag-reorder helper: the pending bubble whose midpoint sits
4351
+ // just below the cursor (the dragged element goes before it).
4352
+ function getDragAfterElement(container, y) {
4353
+ var els = Array.prototype.slice.call(container.querySelectorAll('.walle-msg-pending:not(.dragging)'));
4354
+ var closest = { offset: -Infinity, element: null };
4355
+ els.forEach(function(child) {
4356
+ var box = child.getBoundingClientRect();
4357
+ var offset = y - box.top - box.height / 2;
4358
+ if (offset < 0 && offset > closest.offset) closest = { offset: offset, element: child };
4359
+ });
4360
+ return closest.element;
4361
+ }
4362
+
4363
+ function reorderQueuedItems(id) {
4364
+ var container = document.getElementById('walle-pending-' + id);
4365
+ if (!container) return;
4366
+ var order = Array.prototype.map.call(
4367
+ container.querySelectorAll('.walle-msg-pending'),
4368
+ function(el) { return el.dataset.queueItemId; }
4369
+ ).filter(Boolean);
4370
+ if (order.length) queuePostAction(id, 'reorder', { order: order });
4371
+ }
4372
+
4373
+ function renderPendingQueueBubbles(id) {
4374
+ var ws = getState(id);
4375
+ var messagesArea = document.getElementById('walle-messages-' + id);
4376
+ if (!ws || !messagesArea) return;
4377
+ var container = document.getElementById('walle-pending-' + id);
4378
+ var active = activeQueueItems(ws.queueState);
4379
+ if (!active.length) { if (container) container.remove(); return; }
4380
+ if (!container) {
4381
+ container = document.createElement('div');
4382
+ container.className = 'walle-pending-queue';
4383
+ container.id = 'walle-pending-' + id;
4384
+ }
4385
+ while (container.firstChild) container.removeChild(container.firstChild);
4386
+ messagesArea.appendChild(container); // re-append → stays last (also order:9999 in CSS)
4387
+
4388
+ // Drag-to-reorder: live-move the dragged bubble, persist the new order on drop.
4389
+ container.ondragover = function(ev) {
4390
+ ev.preventDefault();
4391
+ var dragging = container.querySelector('.walle-msg-pending.dragging');
4392
+ if (!dragging) return;
4393
+ var after = getDragAfterElement(container, ev.clientY);
4394
+ if (after == null) container.appendChild(dragging);
4395
+ else if (after !== dragging) container.insertBefore(dragging, after);
4396
+ };
4397
+ container.ondrop = function(ev) { ev.preventDefault(); reorderQueuedItems(id); };
4398
+
4399
+ var reorderable = active.length > 1;
4400
+ active.forEach(function(item, idx) {
4401
+ var sending = String(item.status || '').toLowerCase() === 'sending';
4402
+ var el = document.createElement('div');
4403
+ el.className = 'walle-msg walle-msg-pending' + (sending ? ' sending' : '');
4404
+ el.dataset.queueItemId = item.id;
4405
+ if (reorderable && !sending) {
4406
+ el.draggable = true;
4407
+ el.addEventListener('dragstart', function(ev) {
4408
+ el.classList.add('dragging');
4409
+ try { ev.dataTransfer.effectAllowed = 'move'; ev.dataTransfer.setData('text/plain', item.id); } catch (_) {}
4410
+ });
4411
+ el.addEventListener('dragend', function() { el.classList.remove('dragging'); });
4412
+ }
4413
+
4414
+ var avatar = document.createElement('div');
4415
+ avatar.className = 'walle-msg-avatar user';
4416
+ avatar.textContent = 'U';
4417
+ el.appendChild(avatar);
4418
+
4419
+ var body = document.createElement('div');
4420
+ body.style.flex = '1'; body.style.minWidth = '0';
4421
+
4422
+ var hdr = document.createElement('div');
4423
+ hdr.className = 'walle-msg-header';
4424
+ var name = document.createElement('span');
4425
+ name.className = 'walle-msg-name';
4426
+ name.textContent = 'You';
4427
+ hdr.appendChild(name);
4428
+ var badge = document.createElement('span');
4429
+ badge.className = 'walle-pending-badge';
4430
+ badge.textContent = sending ? 'sending…'
4431
+ : (active.length > 1 ? ('queued · ' + (idx + 1) + '/' + active.length) : 'queued');
4432
+ hdr.appendChild(badge);
4433
+ if (!sending) {
4434
+ var actions = document.createElement('span');
4435
+ actions.className = 'walle-pending-actions';
4436
+ var editBtn = document.createElement('button');
4437
+ editBtn.type = 'button'; editBtn.className = 'walle-pending-action';
4438
+ editBtn.title = 'Edit — move back to the composer'; editBtn.textContent = '✎';
4439
+ editBtn.onclick = (function(it, cnt) { return function(e) { e.preventDefault(); editQueuedItem(id, it, cnt); }; })(item, active.length);
4440
+ actions.appendChild(editBtn);
4441
+ var cancelBtn = document.createElement('button');
4442
+ cancelBtn.type = 'button'; cancelBtn.className = 'walle-pending-action danger';
4443
+ cancelBtn.title = 'Remove from queue'; cancelBtn.textContent = '✕';
4444
+ cancelBtn.onclick = (function(it, cnt) { return function(e) { e.preventDefault(); removeQueuedItem(id, it.id, cnt); }; })(item, active.length);
4445
+ actions.appendChild(cancelBtn);
4446
+ hdr.appendChild(actions);
4447
+ }
4448
+ body.appendChild(hdr);
4449
+
4450
+ var content = document.createElement('div');
4451
+ content.className = 'walle-msg-content walle-pending-content';
4452
+ content.textContent = String(item.text || item.title || 'Queued prompt');
4453
+ body.appendChild(content);
4454
+
4455
+ if (Array.isArray(item.images) && item.images.length) {
4456
+ body.appendChild(renderAttachmentStrip(id, item.images, false));
4457
+ }
4458
+
4459
+ el.appendChild(body);
4460
+ container.appendChild(el);
4461
+ });
4462
+ }
4463
+
4464
+ // A thin one-line status above the composer (replaces the bulky yellow card).
4465
+ // The per-item controls live on the inline bubbles; this just shows count +
4466
+ // mode + cancel-all, and a "Send next" when in manual mode.
4467
+ function renderQueuePreview(id) {
4468
+ var ws = getState(id);
4469
+ var slot = document.getElementById('walle-queue-preview-' + id);
4470
+ if (!ws || !slot) return;
4471
+ while (slot.firstChild) slot.removeChild(slot.firstChild);
4472
+
4473
+ var queue = ws.queueState;
4474
+ var active = activeQueueItems(queue);
4475
+ if (!queue || queue.status === 'idle' || queue.status === 'done' || active.length === 0) {
4476
+ slot.style.display = 'none';
4477
+ return;
4478
+ }
4479
+ var mode = String(queue.mode || 'auto').toLowerCase();
4480
+ var n = active.length;
4481
+
4482
+ var bar = document.createElement('div');
4483
+ bar.className = 'walle-queue-status' + (queue.waitingMessage ? ' waiting' : '');
4484
+
4485
+ var label = document.createElement('span');
4486
+ label.className = 'walle-queue-status-label';
4487
+ label.textContent = queue.waitingMessage
4488
+ ? queue.waitingMessage
4489
+ : (n + (n === 1 ? ' message queued — ' : ' messages queued — ')
4490
+ + (mode === 'manual' ? 'manual send' : 'auto-sends when Wall-E is idle'));
4491
+ bar.appendChild(label);
4492
+
4493
+ var actions = document.createElement('span');
4494
+ actions.className = 'walle-queue-status-actions';
4495
+ if (mode === 'manual') {
4496
+ var nextBtn = document.createElement('button');
4497
+ nextBtn.type = 'button'; nextBtn.className = 'walle-queue-status-btn';
4498
+ nextBtn.textContent = 'Send next';
4499
+ nextBtn.onclick = function() { queuePostAction(id, 'next'); };
4500
+ actions.appendChild(nextBtn);
4501
+ }
4502
+ var modeBtn = document.createElement('button');
4503
+ modeBtn.type = 'button'; modeBtn.className = 'walle-queue-status-btn';
4504
+ modeBtn.textContent = mode === 'manual' ? 'Auto' : 'Manual';
4505
+ modeBtn.title = 'Switch to ' + (mode === 'manual' ? 'auto' : 'manual') + ' sending';
4506
+ modeBtn.onclick = function() { queuePostAction(id, 'mode', { mode: mode === 'manual' ? 'auto' : 'manual' }); };
4507
+ actions.appendChild(modeBtn);
4508
+ var cancelBtn = document.createElement('button');
4509
+ cancelBtn.type = 'button'; cancelBtn.className = 'walle-queue-status-btn danger';
4510
+ cancelBtn.textContent = 'Cancel all';
4511
+ cancelBtn.onclick = function() { cancelQueuedMessages(id); };
4512
+ actions.appendChild(cancelBtn);
4513
+ bar.appendChild(actions);
4514
+
4515
+ slot.style.display = 'block';
4516
+ slot.appendChild(bar);
4517
+ }
4518
+
4519
+ function handleQueueState(msg) {
4520
+ var id = msg && (msg.sessionId || msg.id);
4521
+ if (!id) return;
4522
+ var ws = getState(id);
4523
+ if (!ws) return;
4524
+ if (!shouldApplyQueueState(ws, msg)) return;
4525
+ ws.queueState = msg;
4526
+ renderQueuePreview(id);
4527
+ renderPendingQueueBubbles(id);
4528
+ }
4529
+
4530
+ async function cancelQueuedMessages(id) {
4531
+ var ws = getState(id);
4532
+ try {
4533
+ var res = await fetch(queueApiUrl('/api/queues/' + encodeURIComponent(id)), {
4534
+ method: 'DELETE',
4535
+ headers: { 'Authorization': 'Bearer ' + ((state && state.token) || '') }
4536
+ });
4537
+ if (!res.ok) throw new Error('Queue cancel failed');
4538
+ if (ws) {
4539
+ ws.queueState = { sessionId: id, status: 'idle', items: [] };
4540
+ renderQueuePreview(id);
4541
+ }
4542
+ if (typeof toast === 'function') toast('Queued prompts cancelled', { type: 'info', duration: 1800 });
4543
+ } catch (err) {
4544
+ if (typeof toast === 'function') toast(err && err.message ? err.message : 'Queue cancel failed', { type: 'error' });
4545
+ }
4546
+ }
4547
+
4548
+ async function queueOutboundMessage(id, text, attachments, options) {
4549
+ options = options || {};
4550
+ var ws = getState(id);
4551
+ if (!ws) return { ok: false, error: 'Wall-E session is not connected' };
4552
+ if (ws.isQueueingMessage) return { ok: false, error: 'Already queueing this prompt' };
4553
+ attachments = Array.isArray(attachments) ? attachments : [];
4554
+ text = String(text || '').trim();
4555
+ if (!text && !attachments.length) return { ok: false, error: 'Message is empty' };
4556
+ if (!text && attachments.length) text = attachments.length === 1
4557
+ ? 'Please use the attached image.'
4558
+ : 'Please use the attached images.';
4559
+
4560
+ var queueText = options.outboundText || appendAttachmentReferences(text, attachments);
4561
+ ws.isQueueingMessage = true;
4562
+ try {
4563
+ var res = await fetch(queueApiUrl('/api/queues'), {
4564
+ method: 'POST',
4565
+ headers: {
4566
+ 'Content-Type': 'application/json',
4567
+ 'Authorization': 'Bearer ' + ((state && state.token) || '')
4568
+ },
4569
+ body: JSON.stringify({
4570
+ sessionId: id,
4571
+ mode: 'auto',
4572
+ append: true,
4573
+ strategy: 'append',
4574
+ autoStart: true,
4575
+ items: [{
4576
+ title: queueTitleForText(text || queueText),
4577
+ text: queueText,
4578
+ images: attachments.map(normalizeQueueImage)
4579
+ }]
4580
+ })
4581
+ });
4582
+ var data = await res.json().catch(function() { return {}; });
4583
+ if (!res.ok || !data || !data.sessionId) throw new Error((data && data.error) || 'Failed to queue prompt');
4584
+ ws.queueState = data;
4585
+ renderQueuePreview(id);
4586
+ if (options.recordHistory !== false) recordInputHistory(ws, text);
4587
+ return { ok: true, state: data };
4588
+ } catch (err) {
4589
+ return { ok: false, error: err && err.message ? err.message : String(err || 'Failed to queue prompt') };
4590
+ } finally {
4591
+ ws.isQueueingMessage = false;
4592
+ }
4593
+ }
4594
+
2506
4595
  function sendOutboundMessageNow(id, text, attachments, options, prepared) {
2507
4596
  var ws = getState(id);
2508
4597
  if (!ws) return;
2509
4598
  if (ws.isGenerating) return;
2510
4599
 
2511
4600
  if (options.recordHistory !== false) recordInputHistory(ws, text);
2512
- appendUserMessageToSession(id, text, attachments, options.timestamp);
4601
+ if (options.appendUser !== false) {
4602
+ appendUserMessageToSession(id, text, attachments, options.timestamp);
4603
+ }
2513
4604
 
2514
- var model = ws.selectedModel || '';
4605
+ var model = normalizeSessionModelId(ws.selectedModel || '');
2515
4606
  var modelItem = findModelItem(model) || findModelItem(ws.selectedModelRegistryId || '');
2516
4607
  var session = state.sessions.get(id);
2517
4608
  var meta = (session && session.meta) || {};
@@ -2522,14 +4613,35 @@ window.WalleSession = (function() {
2522
4613
  type: 'walle-message',
2523
4614
  id: id,
2524
4615
  text: prepared.outboundText,
2525
- attachments: attachments.filter(function(a) { return a && a.data; }).map(function(a) {
2526
- return { type: a.type || 'image', name: a.name || a.filename || 'image.png', data: a.data, label: a.label || undefined };
4616
+ // Keep attachments that carry either inline base64 OR a disk reference.
4617
+ // A reloaded branch message has no `data` (branchSafeAttachments strips
4618
+ // it), but the image is still on disk — the server rehydrates the base64
4619
+ // from path/url/filename via _hydrateWalleAttachment.
4620
+ attachments: attachments.filter(function(a) {
4621
+ return a && (a.data || a.url || a.path || a.file_path || a.filename);
4622
+ }).map(function(a) {
4623
+ return {
4624
+ type: a.type || 'image',
4625
+ name: a.name || a.filename || 'image.png',
4626
+ filename: a.filename || a.name || '',
4627
+ data: a.data || '',
4628
+ label: a.label || undefined,
4629
+ url: a.url || '',
4630
+ path: a.path || a.file_path || '',
4631
+ mediaType: a.mediaType || a.mimeType || '',
4632
+ imageWidth: a.imageWidth || null,
4633
+ imageHeight: a.imageHeight || null,
4634
+ originalWidth: a.originalWidth || null,
4635
+ originalHeight: a.originalHeight || null,
4636
+ resizedForProvider: !!a.resizedForProvider
4637
+ };
2527
4638
  }),
2528
4639
  model: model,
2529
4640
  provider: provider,
2530
4641
  modelPinned: modelPinned,
2531
4642
  allowProviderFallback: !modelPinned,
2532
- contextSessionId: resolveContextSessionId(id)
4643
+ contextSessionId: resolveContextSessionId(id),
4644
+ contextMessages: contextMessagesForModel(ws, prepared.outboundText)
2533
4645
  });
2534
4646
 
2535
4647
  ws.isGenerating = true;
@@ -2566,7 +4678,19 @@ window.WalleSession = (function() {
2566
4678
  return { ok: true };
2567
4679
  }
2568
4680
 
2569
- function sendMessage(id) {
4681
+ // ⌘/Ctrl+Enter — "send now". Idle: send live immediately. Busy: interrupt the
4682
+ // current run, then queue the message so it fires the instant Wall-E stops.
4683
+ // (The server drops a walle-message received mid-run, so we route through the
4684
+ // queue's send-when-ready path instead of racing the cancel.)
4685
+ function sendNow(id) {
4686
+ var ws = getState(id);
4687
+ if (!ws || !ws.isGenerating) { sendMessage(id); return; }
4688
+ try { send({ type: 'walle-cancel', id: id }); } catch (_) {}
4689
+ if (typeof toast === 'function') toast('Interrupting Wall-E — sending your message next', { type: 'info' });
4690
+ sendMessage(id); // still generating → queues (auto); fires once the run aborts
4691
+ }
4692
+
4693
+ async function sendMessage(id) {
2570
4694
  var container = document.getElementById('walle-session-' + id);
2571
4695
  if (!container) return;
2572
4696
  var textarea = container.querySelector('.walle-input');
@@ -2577,12 +4701,36 @@ window.WalleSession = (function() {
2577
4701
 
2578
4702
  var ws = getState(id);
2579
4703
  if (!ws) return;
4704
+ // Gate: don't send while a pasted image is still resizing/compressing —
4705
+ // otherwise the text fires without its attachment.
4706
+ if (hasPendingAttachments(ws)) {
4707
+ if (typeof toast === 'function') toast('Image still compressing — one moment…', { type: 'info' });
4708
+ return;
4709
+ }
2580
4710
  var attachments = (ws.inputAttachments || []).slice();
4711
+ if (ws.isGenerating) {
4712
+ var queued = await queueOutboundMessage(id, text, attachments);
4713
+ if (!queued.ok) {
4714
+ if (queued.error !== 'Message is empty' && typeof toast === 'function') {
4715
+ toast(queued.error || 'Failed to queue prompt', { type: 'error' });
4716
+ }
4717
+ return;
4718
+ }
4719
+ textarea.value = '';
4720
+ clearComposerDraft(id, textarea);
4721
+ syncComposerChrome(id, textarea);
4722
+ ws.inputAttachments = [];
4723
+ renderComposerAttachments(id);
4724
+ focusInput(id);
4725
+ return;
4726
+ }
4727
+
2581
4728
  var result = sendOutboundMessage(id, text, attachments);
2582
4729
  if (!result.ok) return;
2583
4730
 
2584
4731
  // Clear input
2585
4732
  textarea.value = '';
4733
+ clearComposerDraft(id, textarea);
2586
4734
  syncComposerChrome(id, textarea);
2587
4735
  ws.inputAttachments = [];
2588
4736
  renderComposerAttachments(id);
@@ -2599,6 +4747,12 @@ window.WalleSession = (function() {
2599
4747
  function handleUser(msg) {
2600
4748
  var id = msg.id;
2601
4749
  var ws = getState(id);
4750
+ try {
4751
+ console.log('[queue-diag] client handleUser id=' + id + ' hasWs=' + !!ws
4752
+ + ' contentLen=' + String(msg.content || msg.text || '').length
4753
+ + ' areaExists=' + !!document.getElementById('walle-messages-' + id)
4754
+ + ' activeTab=' + (typeof state !== 'undefined' && state.activeTab === id));
4755
+ } catch (_) {}
2602
4756
  if (!ws) return;
2603
4757
  appendUserMessageToSession(id, msg.content || msg.text || '', msg.attachments || [], msg.timestamp);
2604
4758
  updateHeaderStats(id);
@@ -2712,9 +4866,18 @@ window.WalleSession = (function() {
2712
4866
 
2713
4867
  case 'permission_request':
2714
4868
  removeThinking(messagesArea);
2715
- var permCard = renderPermissionCard(id, ev);
2716
- messagesArea.appendChild(permCard);
2717
- scrollToBottom(messagesArea);
4869
+ if (!ev.permId || !messagesArea.querySelector('#walle-perm-' + ev.permId)) {
4870
+ var permCard = renderPermissionCard(id, ev);
4871
+ messagesArea.appendChild(permCard);
4872
+ scrollToBottom(messagesArea);
4873
+ }
4874
+ break;
4875
+
4876
+ case 'permission_resolved':
4877
+ if (ev.permId) {
4878
+ var resolvedCard = messagesArea.querySelector('#walle-perm-' + ev.permId);
4879
+ if (resolvedCard) markPermissionCardResolved(resolvedCard, ev.decision || 'allow', '');
4880
+ }
2718
4881
  break;
2719
4882
  }
2720
4883
  }
@@ -2751,12 +4914,20 @@ window.WalleSession = (function() {
2751
4914
  ws.isGenerating = false;
2752
4915
  recordLiveRoleDelivery(ws, 'assistant');
2753
4916
 
2754
- renderMessage(messagesArea, assistantMsg);
4917
+ if (restartVisibleMessageRenderIfBusy(id, ws)) {
4918
+ updateSendButton(id, false);
4919
+ updateHeaderStats(id);
4920
+ return;
4921
+ }
4922
+
4923
+ renderMessage(messagesArea, assistantMsg, ws.messages.length - 1);
4924
+ if (hasBranchSnapshot(ws)) saveWalleBranchSnapshot(id);
2755
4925
  syncPromptMessageDomIndices(id);
2756
4926
  updatePromptNav(id);
2757
4927
  scrollToBottom(messagesArea);
2758
4928
  updateSendButton(id, false);
2759
4929
  updateHeaderStats(id);
4930
+ renderWorkBar(id);
2760
4931
  focusInput(id);
2761
4932
  }
2762
4933
 
@@ -2765,40 +4936,79 @@ window.WalleSession = (function() {
2765
4936
  var id = msg.id;
2766
4937
  var ws = getState(id);
2767
4938
  if (!ws) return;
4939
+ var requestId = Number(msg.historyRequestId || 0);
4940
+ var currentRequestId = Number(ws.historyRequestId || 0);
4941
+ if (requestId && currentRequestId && requestId < currentRequestId) return;
2768
4942
  var composerSnapshot = captureComposerState(id);
4943
+ var liveTurn = captureLiveTurnState(id, ws);
4944
+ var messages = Array.isArray(msg.messages) ? msg.messages.slice() : [];
4945
+ if (liveTurn) {
4946
+ messages = messages.filter(function(m) { return !isOpenTurnActivityMessage(m); });
4947
+ }
4948
+ if (msg.error && (!Array.isArray(messages) || messages.length === 0)) {
4949
+ ws.historyStatus = 'error';
4950
+ ws.historyError = msg.error;
4951
+ var errSession = state.sessions.get(id);
4952
+ if (errSession) {
4953
+ errSession._walleHistoryLoading = false;
4954
+ errSession._walleHistoryLoaded = false;
4955
+ }
4956
+ renderSession(id);
4957
+ restoreComposerState(id, composerSnapshot);
4958
+ return;
4959
+ }
2769
4960
 
2770
4961
  var session = state.sessions.get(id);
2771
4962
  if (session) {
2772
4963
  session.needsAttach = false;
2773
4964
  session._walleHistoryRequested = true;
4965
+ session._walleHistoryLoading = false;
4966
+ session._walleHistoryLoaded = true;
2774
4967
  }
4968
+ markHistoryLoaded(id);
2775
4969
 
2776
4970
  ws.messages = [];
2777
4971
  ws.messageCount = 0;
2778
4972
  ws.totalCost = 0;
2779
4973
  ws._currentAssistant = null;
2780
4974
 
2781
- var messages = msg.messages || [];
2782
4975
  for (var i = 0; i < messages.length; i++) {
2783
4976
  var m = messages[i];
2784
- ws.messages.push({
4977
+ var normalized = {
2785
4978
  role: m.role,
2786
- content: m.content || '',
4979
+ content: m.content || m.text || '',
2787
4980
  model: m.model_id || m.model || '',
2788
4981
  latency_ms: m.latency_ms || 0,
2789
4982
  timestamp: m.created_at || m.timestamp || 0,
2790
- toolCalls: m.toolCalls || []
2791
- });
4983
+ toolCalls: m.toolCalls || [],
4984
+ attachments: Array.isArray(m.attachments) ? m.attachments : []
4985
+ };
4986
+ if (m.liveActivity) normalized.liveActivity = true;
4987
+ if (m.agentLabel) normalized.agentLabel = m.agentLabel;
4988
+ if (m.metadata) normalized.metadata = cloneJsonSafe(m.metadata, m.metadata);
4989
+ if (m.runtimeDiagnostic) normalized.runtimeDiagnostic = cloneJsonSafe(m.runtimeDiagnostic, m.runtimeDiagnostic);
4990
+ ws.messages.push(normalized);
2792
4991
  ws.messageCount++;
2793
4992
  }
4993
+ if (liveTurn) {
4994
+ ws.isGenerating = true;
4995
+ ws._currentAssistant = liveTurn.currentAssistant;
4996
+ }
4997
+ var durableHistory = activeHistorySnapshot(ws);
4998
+ applyBranchHistoryIfAuthoritative(ws, durableHistory);
2794
4999
  seedInputHistoryFromMessages(ws);
2795
5000
 
2796
- // Re-render if the session container exists (don't gate on activeTab —
2797
- // history may arrive before the tab is activated on reconnect)
2798
- if (document.getElementById('walle-session-' + id)) {
5001
+ ws.renderPending = true;
5002
+
5003
+ // Re-render only when visible. A large Wall-E replay must not monopolize
5004
+ // the browser while the user has already moved to an unrelated session.
5005
+ if (document.getElementById('walle-session-' + id) && isWalleSessionVisible(id)) {
2799
5006
  renderSession(id);
2800
5007
  restoreComposerState(id, composerSnapshot);
2801
5008
  ensurePendingThinking(id);
5009
+ if (liveTurn && liveTurn.currentAssistant) {
5010
+ refreshLiveActivity(document.getElementById('walle-messages-' + id), id, ws);
5011
+ }
2802
5012
  }
2803
5013
  updatePromptNav(id);
2804
5014
  }
@@ -2855,6 +5065,10 @@ window.WalleSession = (function() {
2855
5065
  message: issue.userMessage || issue.message || source.message || source.error || 'Wall-E could not get a response from the configured AI provider.',
2856
5066
  rawMessage: issue.rawMessage || '',
2857
5067
  provider: issue.provider || '',
5068
+ providerId: issue.providerId || issue.provider_id || '',
5069
+ registryId: issue.registryId || issue.registry_id || '',
5070
+ routeLabel: issue.routeLabel || issue.route_label || '',
5071
+ connectionKind: issue.connectionKind || issue.connection_kind || '',
2858
5072
  model: issue.model || '',
2859
5073
  status: issue.status || '',
2860
5074
  retryAfter: issue.retryAfter || '',
@@ -2878,6 +5092,10 @@ window.WalleSession = (function() {
2878
5092
 
2879
5093
  var detailLines = [];
2880
5094
  if (issue.status) detailLines.push('HTTP/status: ' + issue.status);
5095
+ if (issue.routeLabel) detailLines.push('Route: ' + issue.routeLabel);
5096
+ if (issue.providerId) detailLines.push('Provider ID: ' + issue.providerId);
5097
+ if (issue.registryId) detailLines.push('Registry ID: ' + issue.registryId);
5098
+ if (issue.connectionKind) detailLines.push('Connection: ' + issue.connectionKind);
2881
5099
  if (issue.provider) detailLines.push('Provider: ' + issue.provider);
2882
5100
  if (issue.model) detailLines.push('Model: ' + issue.model);
2883
5101
  if (issue.retryAfter) detailLines.push('Retry after: ' + issue.retryAfter);
@@ -2928,6 +5146,7 @@ window.WalleSession = (function() {
2928
5146
  messagesArea.appendChild(notice);
2929
5147
  scrollToBottom(messagesArea);
2930
5148
  updateSendButton(id, false);
5149
+ renderWorkBar(id);
2931
5150
  focusInput(id);
2932
5151
  }
2933
5152
 
@@ -2943,7 +5162,11 @@ window.WalleSession = (function() {
2943
5162
  return Promise.resolve(_walleModelRegistryCache);
2944
5163
  }
2945
5164
  if (_walleModelRegistryPromise) return _walleModelRegistryPromise;
2946
- _walleModelRegistryPromise = fetch('/api/models/coding-availability', { cache: 'no-store' })
5165
+ _walleModelRegistryPromise = fetch('/api/models/coding-catalog', { cache: 'no-store' })
5166
+ .then(function(r) {
5167
+ if (r.status === 404) return fetch('/api/models/coding-availability', { cache: 'no-store' });
5168
+ return r;
5169
+ })
2947
5170
  .then(function(r) {
2948
5171
  if (!r.ok) throw new Error('HTTP ' + r.status);
2949
5172
  return r.json();
@@ -2952,9 +5175,10 @@ window.WalleSession = (function() {
2952
5175
  if (data && data.error) throw new Error(data.error);
2953
5176
  var rows = Array.isArray(data) ? data : (Array.isArray(data?.models) ? data.models : []);
2954
5177
  _walleModelAvailabilityMeta = {
2955
- source: data?.source || 'live',
5178
+ source: data?.source || 'catalog',
2956
5179
  generatedAt: data?.generated_at || '',
2957
5180
  providers: Array.isArray(data?.providers) ? data.providers : [],
5181
+ counts: data?.counts || null,
2958
5182
  error: null
2959
5183
  };
2960
5184
  _walleModelRegistryCache = normalizeWalleModels(rows);
@@ -2964,9 +5188,10 @@ window.WalleSession = (function() {
2964
5188
  .catch(function(err) {
2965
5189
  invalidateWalleModelCache();
2966
5190
  _walleModelAvailabilityMeta = {
2967
- source: 'live',
5191
+ source: 'catalog',
2968
5192
  generatedAt: '',
2969
5193
  providers: [],
5194
+ counts: null,
2970
5195
  error: err && err.message ? err.message : String(err || 'Failed to load models')
2971
5196
  };
2972
5197
  return [];
@@ -2990,7 +5215,11 @@ window.WalleSession = (function() {
2990
5215
  }
2991
5216
 
2992
5217
  function stripModelAlias(label) {
2993
- return String(label || '').replace(/@\d{6,8}\b/g, '').replace(/\s+/g, ' ').trim();
5218
+ return String(label || '').replace(/@(default|latest|\d{6,8})\b/gi, '').replace(/\s+/g, ' ').trim();
5219
+ }
5220
+
5221
+ function normalizeSessionModelId(modelId) {
5222
+ return String(modelId || '').trim().replace(/@(default|latest|20\d{6})$/i, '');
2994
5223
  }
2995
5224
 
2996
5225
  function isLegacyModel(m) {
@@ -3002,13 +5231,19 @@ window.WalleSession = (function() {
3002
5231
  for (var i = 0; i < models.length; i++) {
3003
5232
  var m = models[i];
3004
5233
  if (!m || m.enabled === 0 || m.enabled === false) continue;
3005
- if (m.available === false || m.coding_capable === false) continue;
5234
+ if (m.selectable === false) continue;
5235
+ if (m.selectable === undefined && (m.available === false || m.coding_capable === false)) continue;
3006
5236
  var provider = m.provider_type || m.provider || '';
3007
5237
  var label = String(m.display_name || m.model_id || m.id || '').trim();
3008
5238
  if (!label) continue;
5239
+ var modelId = normalizeSessionModelId(m.model_id || m.modelId || m.id);
5240
+ var catalogId = m.catalog_id || m.catalogId || ((provider || 'provider') + '|' + modelId);
3009
5241
  enabled.push({
3010
- id: m.id,
3011
- modelId: m.model_id || m.id,
5242
+ id: catalogId,
5243
+ catalogId: catalogId,
5244
+ registryId: m.registry_id || m.registryId || '',
5245
+ preferredRegistryId: m.preferred_registry_id || m.preferredRegistryId || '',
5246
+ modelId: modelId,
3012
5247
  label: label,
3013
5248
  baseLabel: stripModelAlias(label),
3014
5249
  provider: provider,
@@ -3016,6 +5251,14 @@ window.WalleSession = (function() {
3016
5251
  providerLabel: m.provider_name || providerLabel(provider),
3017
5252
  capabilities: normalizeModelCapabilities(m.capabilities),
3018
5253
  source: m.source || 'live',
5254
+ availabilityState: m.availability_state || m.availabilityState || (m.live_available ? 'available' : 'configured'),
5255
+ liveAvailable: m.live_available === true || m.source === 'live' || m.source === 'live+registry',
5256
+ routeMode: m.route_mode || m.routeMode || 'policy',
5257
+ routeLabel: m.route_label || m.routeLabel || '',
5258
+ routePolicy: m.route_policy || m.routePolicy || '',
5259
+ routeProviderId: m.route_provider_id || m.routeProviderId || '',
5260
+ routeProviderName: m.route_provider_name || m.routeProviderName || '',
5261
+ routeConnectionKind: m.route_connection_kind || m.routeConnectionKind || '',
3019
5262
  codingCapable: m.coding_capable !== false,
3020
5263
  codingReason: m.coding_reason || '',
3021
5264
  speedTier: m.speed_tier || 3,
@@ -3055,7 +5298,7 @@ window.WalleSession = (function() {
3055
5298
  var byKey = {};
3056
5299
  for (var i = 0; i < items.length; i++) {
3057
5300
  var item = items[i];
3058
- var key = (item.provider || '') + '|' + item.baseLabel.toLowerCase();
5301
+ var key = item.catalogId || ((item.provider || '') + '|' + item.modelId);
3059
5302
  if (!byKey[key]) byKey[key] = item;
3060
5303
  }
3061
5304
  return Object.keys(byKey).map(function(k) { return byKey[k]; });
@@ -3149,7 +5392,7 @@ window.WalleSession = (function() {
3149
5392
  var id = String(modelId);
3150
5393
  for (var i = 0; i < _walleModelRegistryCache.length; i++) {
3151
5394
  var item = _walleModelRegistryCache[i];
3152
- if (item.id === id || item.modelId === id) return item;
5395
+ if (item.id === id || item.catalogId === id || item.modelId === id || item.registryId === id || item.preferredRegistryId === id) return item;
3153
5396
  }
3154
5397
  return null;
3155
5398
  }
@@ -3157,6 +5400,7 @@ window.WalleSession = (function() {
3157
5400
  function setSelectedModel(id, item) {
3158
5401
  var ws = getState(id);
3159
5402
  if (!ws) return;
5403
+ var composerSnapshot = captureComposerState(id);
3160
5404
  ws.selectedModel = item ? item.modelId : '';
3161
5405
  ws.selectedModelRegistryId = item ? item.id : '';
3162
5406
  ws.selectedModelLabel = item ? item.baseLabel : '';
@@ -3165,19 +5409,23 @@ window.WalleSession = (function() {
3165
5409
  ws._modelManual = true;
3166
5410
  syncWalleModelButtons(id);
3167
5411
  if (item && typeof send === 'function') {
5412
+ var exactRegistryId = item.routeMode === 'exact' ? (item.registryId || item.preferredRegistryId || '') : '';
3168
5413
  send({
3169
5414
  type: 'model-change',
3170
5415
  id: id,
3171
5416
  agent_type: 'walle',
3172
5417
  model_id: item.modelId,
3173
5418
  model_provider: item.provider,
3174
- model_registry_id: item.id || '',
3175
- model_provider_id: item.providerId || '',
5419
+ model_registry_id: exactRegistryId,
5420
+ model_provider_id: '',
5421
+ model_route_mode: item.routeMode || 'policy',
3176
5422
  scope: 'session',
3177
5423
  pinned: true,
3178
5424
  });
3179
5425
  }
3180
5426
  closeModelPicker();
5427
+ restoreComposerState(id, composerSnapshot);
5428
+ focusInput(id);
3181
5429
  }
3182
5430
 
3183
5431
  function closeModelPicker() {
@@ -3273,7 +5521,19 @@ window.WalleSession = (function() {
3273
5521
  oldTag.textContent = 'Versioned';
3274
5522
  tags.appendChild(oldTag);
3275
5523
  }
3276
- if (item && item.source === 'live') {
5524
+ if (item && item.routeConnectionKind) {
5525
+ var routeTag = document.createElement('span');
5526
+ routeTag.className = 'walle-model-tag muted';
5527
+ routeTag.textContent = item.routeConnectionKind === 'portkey' ? 'Portkey' : item.routeConnectionKind.replace(/_/g, ' ');
5528
+ tags.appendChild(routeTag);
5529
+ }
5530
+ if (item && item.availabilityState === 'route_issue') {
5531
+ var issueTag = document.createElement('span');
5532
+ issueTag.className = 'walle-model-tag muted';
5533
+ issueTag.textContent = 'Route issue';
5534
+ tags.appendChild(issueTag);
5535
+ }
5536
+ if (item && item.liveAvailable) {
3277
5537
  var liveTag = document.createElement('span');
3278
5538
  liveTag.className = 'walle-model-tag accent';
3279
5539
  liveTag.textContent = 'Live';
@@ -3287,30 +5547,37 @@ window.WalleSession = (function() {
3287
5547
  var q = search.value.trim().toLowerCase();
3288
5548
  var filtered = models.filter(function(item) {
3289
5549
  if (!q) return true;
3290
- return (item.label + ' ' + item.modelId + ' ' + item.providerLabel + ' ' + (item.capabilities || []).join(' ')).toLowerCase().indexOf(q) !== -1;
5550
+ return (item.label + ' ' + item.modelId + ' ' + item.providerLabel + ' ' + item.routeLabel + ' ' + item.routeProviderName + ' ' + (item.capabilities || []).join(' ')).toLowerCase().indexOf(q) !== -1;
3291
5551
  });
3292
5552
  return dedupeVisibleModels(filtered);
3293
5553
  }
3294
5554
 
3295
5555
  function providerStatusSummary() {
3296
5556
  var meta = _walleModelAvailabilityMeta || {};
5557
+ var counts = meta.counts || {};
3297
5558
  var providers = Array.isArray(meta.providers) ? meta.providers : [];
3298
5559
  var available = providers.filter(function(provider) { return provider.status === 'available'; }).length;
3299
5560
  var unavailable = providers.filter(function(provider) {
3300
5561
  return provider.status && provider.status !== 'available' && provider.status !== 'empty';
3301
5562
  }).length;
3302
- if (meta.error) return 'Live coding models unavailable';
3303
- if (available > 0) return models.length + ' live coding models · ' + available + ' provider' + (available === 1 ? '' : 's');
3304
- if (unavailable > 0) return 'No live coding models · check provider setup';
3305
- return 'Live coding models';
5563
+ if (meta.error) return 'Model catalog unavailable';
5564
+ if (counts && counts.models !== undefined) {
5565
+ var parts = [models.length + ' models'];
5566
+ if (counts.configured_routes) parts.push(counts.configured_routes + ' configured routes');
5567
+ if (counts.live) parts.push(counts.live + ' live');
5568
+ return parts.join(' · ');
5569
+ }
5570
+ if (available > 0) return models.length + ' models · ' + available + ' live provider' + (available === 1 ? '' : 's');
5571
+ if (unavailable > 0) return models.length + ' models · route health in AI Providers';
5572
+ return models.length + ' models';
3306
5573
  }
3307
5574
 
3308
5575
  function emptyModelMessage(hasQuery) {
3309
- if (hasQuery) return 'No matching live coding models';
5576
+ if (hasQuery) return 'No matching models';
3310
5577
  if (_walleModelAvailabilityMeta && _walleModelAvailabilityMeta.error) {
3311
- return 'Could not load live coding models';
5578
+ return 'Could not load model catalog';
3312
5579
  }
3313
- return 'No live coding models available';
5580
+ return 'No configured models available';
3314
5581
  }
3315
5582
 
3316
5583
  function renderLoading() {
@@ -3319,7 +5586,7 @@ window.WalleSession = (function() {
3319
5586
  loading.className = 'walle-model-empty';
3320
5587
  loading.textContent = 'Loading models...';
3321
5588
  body.appendChild(loading);
3322
- sourceNote.textContent = 'Checking providers...';
5589
+ sourceNote.textContent = 'Loading model catalog...';
3323
5590
  }
3324
5591
 
3325
5592
  function renderList() {
@@ -3421,7 +5688,11 @@ window.WalleSession = (function() {
3421
5688
  closeModelPicker();
3422
5689
  },
3423
5690
  onKeyDown: function(e) {
3424
- if (e.key === 'Escape') closeModelPicker();
5691
+ if (e.key === 'Escape') {
5692
+ e.preventDefault();
5693
+ e.stopPropagation();
5694
+ closeModelPicker();
5695
+ }
3425
5696
  },
3426
5697
  onResize: function() {
3427
5698
  positionModelPicker(anchor, popover);
@@ -3632,12 +5903,18 @@ window.WalleSession = (function() {
3632
5903
  focusInput(id);
3633
5904
  } else {
3634
5905
  // Show send button
5906
+ var pendingAttach = hasPendingAttachments(getState(id));
3635
5907
  if (btn) {
3636
5908
  btn.textContent = '\u2191';
3637
5909
  btn.style.background = '';
3638
- btn.title = 'Send message';
3639
- btn.setAttribute('aria-label', 'Send message');
5910
+ btn.setAttribute('aria-label', pendingAttach ? 'Waiting for image' : 'Send message');
3640
5911
  btn.onclick = function() { sendMessage(id); };
5912
+ // Gate send until a pasted image finishes compressing, so the message
5913
+ // can't fire without its attachment.
5914
+ btn.disabled = pendingAttach;
5915
+ btn.title = pendingAttach ? 'Waiting for image to finish compressing\u2026' : 'Send message';
5916
+ btn.style.opacity = pendingAttach ? '0.5' : '';
5917
+ btn.style.cursor = pendingAttach ? 'progress' : '';
3641
5918
  }
3642
5919
  if (input) input.disabled = false;
3643
5920
  if (attach) attach.disabled = false;
@@ -3663,7 +5940,11 @@ window.WalleSession = (function() {
3663
5940
  var cwd = s?.meta?.cwd || '';
3664
5941
  var parts = [];
3665
5942
  if (cwd) parts.push(cwd);
3666
- parts.push(ws.messageCount + ' messages');
5943
+ if (historyStatusForSession(id, ws) === 'loading' && (ws.messageCount || 0) === 0) {
5944
+ parts.push('Loading messages...');
5945
+ } else {
5946
+ parts.push(ws.messageCount + ' messages');
5947
+ }
3667
5948
  meta.textContent = parts.join(' ');
3668
5949
  }
3669
5950
 
@@ -3700,7 +5981,7 @@ window.WalleSession = (function() {
3700
5981
  && ws.selectedModel === msg.model_id
3701
5982
  && (!msg.model_provider || ws.selectedModelProviderType === msg.model_provider);
3702
5983
  if (ws._modelManual && !sameManualModel) return;
3703
- ws.selectedModel = msg.model_id;
5984
+ ws.selectedModel = normalizeSessionModelId(msg.model_id);
3704
5985
  ws.selectedModelRegistryId = msg.model_registry_id || msg.modelRegistryId || '';
3705
5986
  ws.selectedModelLabel = '';
3706
5987
  ws.selectedModelProviderType = msg.model_provider || ws.selectedModelProviderType || '';
@@ -3712,9 +5993,16 @@ window.WalleSession = (function() {
3712
5993
  // ---------- public API ----------
3713
5994
  return {
3714
5995
  getState: getState,
5996
+ requestHistory: requestHistory,
5997
+ markHistoryLoading: markHistoryLoading,
5998
+ markHistoryLoaded: markHistoryLoaded,
5999
+ historyStatusForSession: historyStatusForSession,
6000
+ messageRenderNeedsRestart: messageRenderNeedsRestart,
3715
6001
  renderSession: renderSession,
3716
6002
  sendMessage: sendMessage,
3717
6003
  sendQueuedMessage: sendQueuedMessage,
6004
+ handleQueueState: handleQueueState,
6005
+ handleWorkState: handleWorkState,
3718
6006
  handleUser: handleUser,
3719
6007
  handleProgress: handleProgress,
3720
6008
  handleResponse: handleResponse,
@@ -3722,6 +6010,15 @@ window.WalleSession = (function() {
3722
6010
  handleTranscriptAppend: handleTranscriptAppend,
3723
6011
  handleError: handleError,
3724
6012
  handleModel: handleModel,
6013
+ handleGlobalEscape: handleGlobalEscape,
6014
+ confirmCancelActiveRun: confirmCancelActiveRun,
6015
+ editMessage: editMessage,
6016
+ cancelEdit: cancelEdit,
6017
+ submitEdit: submitEdit,
6018
+ deleteFromPrompt: deleteFromPrompt,
6019
+ switchBranch: switchBranch,
6020
+ saveWalleBranchSnapshot: saveWalleBranchSnapshot,
6021
+ branchSafeAttachments: branchSafeAttachments,
3725
6022
  promptNavGo: promptNavGo,
3726
6023
  promptNavToggleList: promptNavToggleList,
3727
6024
  updatePromptNav: updatePromptNav,