create-walle 0.9.11 → 0.9.13

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 (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -46,7 +46,8 @@ function initSetupTabs() {
46
46
  function _keyInputIdFor(type) {
47
47
  return type === 'anthropic' ? 'setup-api-key'
48
48
  : type === 'openai' ? 'setup-openai-key'
49
- : type === 'google' ? 'setup-google-key' : null;
49
+ : type === 'google' ? 'setup-google-key'
50
+ : type === 'deepseek' ? 'setup-deepseek-key' : null;
50
51
  }
51
52
 
52
53
  function initProviderPicker() {
@@ -71,20 +72,32 @@ function initProviderPicker() {
71
72
  document.querySelectorAll('#setup-panel button[data-default-btn]').forEach(function(btn) {
72
73
  btn.addEventListener('click', async function() {
73
74
  var type = btn.getAttribute('data-default-btn');
75
+ var modelEl = document.getElementById(_selectIdFor(type));
76
+ var previousDefault = document.querySelector('#setup-panel [data-default-btn].is-default')?.getAttribute('data-default-btn') || null;
77
+ _renderDefaultBadge(type);
78
+ btn.classList.add('is-saving');
79
+ document.querySelectorAll('#setup-panel button[data-default-btn]').forEach(function(b) { b.disabled = true; });
74
80
  try {
75
81
  var r = await fetch('/api/setup/set-default', {
76
82
  method: 'POST', headers: { 'Content-Type': 'application/json' },
77
- body: JSON.stringify({ type: type }),
83
+ body: JSON.stringify({ type: type, model: modelEl ? modelEl.value : '' }),
78
84
  });
79
85
  var d = await r.json();
80
86
  if (d.ok) {
81
- setupToast(type + ' set as default');
82
- _renderDefaultBadge(type);
87
+ if (modelEl && modelEl.value) _rememberProviderModel(type, modelEl.value);
88
+ setupToast(type + ' set as default' + (d.live_updated ? '' : ' (restart scheduled)'));
83
89
  loadHealthDashboard();
84
90
  } else {
91
+ _renderDefaultBadge(previousDefault);
85
92
  setupToast(d.error || 'Failed to set default', 'error');
86
93
  }
87
- } catch (e) { setupToast(e.message, 'error'); }
94
+ } catch (e) {
95
+ _renderDefaultBadge(previousDefault);
96
+ setupToast(e.message, 'error');
97
+ } finally {
98
+ btn.classList.remove('is-saving');
99
+ document.querySelectorAll('#setup-panel button[data-default-btn]').forEach(function(b) { b.disabled = false; });
100
+ }
88
101
  });
89
102
  });
90
103
 
@@ -130,8 +143,8 @@ function initProviderPicker() {
130
143
  });
131
144
 
132
145
  // API key input → debounced model refresh (preserves prior behavior)
133
- ['setup-api-key', 'setup-openai-key', 'setup-google-key'].forEach(function(id) {
134
- var providerByInput = { 'setup-api-key': 'anthropic', 'setup-openai-key': 'openai', 'setup-google-key': 'google' };
146
+ ['setup-api-key', 'setup-openai-key', 'setup-google-key', 'setup-deepseek-key'].forEach(function(id) {
147
+ var providerByInput = { 'setup-api-key': 'anthropic', 'setup-openai-key': 'openai', 'setup-google-key': 'google', 'setup-deepseek-key': 'deepseek' };
135
148
  var input = document.getElementById(id);
136
149
  if (!input) return;
137
150
  input.addEventListener('input', function() { scheduleKeyChange(providerByInput[id], id); });
@@ -214,6 +227,7 @@ async function saveProviderCard(type) {
214
227
  });
215
228
  var d = await r.json();
216
229
  if (d.ok) {
230
+ if (model) _applyProviderModelSelection(type, model);
217
231
  if (resultEl) { resultEl.className = 'test-result ok'; resultEl.textContent = '✓ Saved'; }
218
232
  setTimeout(function() { if (resultEl) { resultEl.style.display = 'none'; } }, 4000);
219
233
  loadHealthDashboard();
@@ -229,6 +243,7 @@ async function saveProviderCard(type) {
229
243
  // Cache: { '<provider>:<keyFingerprint>': [{id,name,capabilities}, ...] }
230
244
  var _modelsCache = {};
231
245
  var _keyDebounceTimers = {};
246
+ var _providerModelPrefs = {};
232
247
 
233
248
  function _keyFingerprint(key) {
234
249
  if (!key) return 'env';
@@ -241,9 +256,49 @@ function _selectIdFor(provider) {
241
256
  return provider === 'openai' ? 'setup-openai-model'
242
257
  : provider === 'google' ? 'setup-google-model'
243
258
  : provider === 'ollama' ? 'setup-ollama-model'
259
+ : provider === 'deepseek' ? 'setup-deepseek-model'
244
260
  : 'setup-walle-model';
245
261
  }
246
262
 
263
+ function _rememberProviderModel(provider, model) {
264
+ if (!provider) return;
265
+ if (model) _providerModelPrefs[provider] = model;
266
+ else delete _providerModelPrefs[provider];
267
+ }
268
+
269
+ function _preferredModelFor(provider, sel) {
270
+ return _providerModelPrefs[provider] || (sel ? sel.value : '');
271
+ }
272
+
273
+ function _applyProviderModelSelection(provider, model) {
274
+ if (!model) return;
275
+ _rememberProviderModel(provider, model);
276
+ var sel = document.getElementById(_selectIdFor(provider));
277
+ if (!sel) return;
278
+ for (var i = 0; i < sel.options.length; i++) {
279
+ if (sel.options[i].value === model) {
280
+ sel.value = model;
281
+ return;
282
+ }
283
+ }
284
+ var opt = document.createElement('option');
285
+ opt.value = model;
286
+ opt.textContent = model + ' (custom)';
287
+ sel.insertBefore(opt, sel.firstChild);
288
+ sel.value = model;
289
+ }
290
+
291
+ function initProviderModelPreferenceTracking() {
292
+ ['anthropic', 'openai', 'google', 'ollama', 'deepseek'].forEach(function(provider) {
293
+ var sel = document.getElementById(_selectIdFor(provider));
294
+ if (!sel || sel.dataset.modelPrefBound === '1') return;
295
+ sel.dataset.modelPrefBound = '1';
296
+ sel.addEventListener('change', function() {
297
+ _rememberProviderModel(provider, sel.value);
298
+ });
299
+ });
300
+ }
301
+
247
302
  async function loadModels(provider, opts) {
248
303
  opts = opts || {};
249
304
  var sel = document.getElementById(_selectIdFor(provider));
@@ -251,7 +306,7 @@ async function loadModels(provider, opts) {
251
306
  var keyOverride = (opts.apiKey || '').trim();
252
307
  var cacheKey = provider + ':' + _keyFingerprint(keyOverride);
253
308
  if (_modelsCache[cacheKey] && !opts.force) {
254
- _renderModelOptions(sel, _modelsCache[cacheKey]);
309
+ _renderModelOptions(sel, _modelsCache[cacheKey], _preferredModelFor(provider, sel));
255
310
  return;
256
311
  }
257
312
  // Don't capture sel.value here — loadStatus may set the saved model AFTER this
@@ -273,7 +328,7 @@ async function loadModels(provider, opts) {
273
328
  var models = (d && Array.isArray(d.models)) ? d.models : [];
274
329
  if (models.length > 0) {
275
330
  _modelsCache[cacheKey] = models;
276
- _renderModelOptions(sel, models);
331
+ _renderModelOptions(sel, models, _preferredModelFor(provider, sel));
277
332
  }
278
333
  } catch { /* keep existing options on error */ }
279
334
  sel.disabled = false;
@@ -370,21 +425,19 @@ async function loadStatus() {
370
425
  document.getElementById('setup-api-key').placeholder = '\u2022\u2022\u2022\u2022\u2022 (configured)';
371
426
  }
372
427
  if (d.walle_model) {
373
- var selId = activeProvider === 'openai' ? 'setup-openai-model'
374
- : activeProvider === 'google' ? 'setup-google-model'
375
- : activeProvider === 'ollama' ? 'setup-ollama-model'
376
- : 'setup-walle-model';
428
+ var selId = _selectIdFor(activeProvider);
377
429
  var sel = document.getElementById(selId);
378
430
  if (sel) {
431
+ var preferredModel = (activeProvider && _providerModelPrefs[activeProvider]) || d.walle_model;
379
432
  var found = false;
380
433
  for (var i = 0; i < sel.options.length; i++) {
381
- if (sel.options[i].value === d.walle_model) { sel.value = d.walle_model; found = true; break; }
434
+ if (sel.options[i].value === preferredModel) { sel.value = preferredModel; found = true; break; }
382
435
  }
383
436
  if (!found) {
384
437
  var opt = document.createElement('option');
385
- opt.value = d.walle_model; opt.textContent = d.walle_model;
438
+ opt.value = preferredModel; opt.textContent = preferredModel;
386
439
  sel.insertBefore(opt, sel.firstChild);
387
- sel.value = d.walle_model;
440
+ sel.value = preferredModel;
388
441
  }
389
442
  }
390
443
  }
@@ -446,6 +499,9 @@ async function saveConfig() {
446
499
  modelVal = document.getElementById('setup-google-model').value;
447
500
  } else if (_selectedProvider === 'ollama') {
448
501
  modelVal = document.getElementById('setup-ollama-model').value;
502
+ } else if (_selectedProvider === 'deepseek') {
503
+ apiVal = document.getElementById('setup-deepseek-key').value.trim();
504
+ modelVal = document.getElementById('setup-deepseek-model').value;
449
505
  }
450
506
 
451
507
  if (!ownerVal) {
@@ -1589,6 +1645,7 @@ SETUP.init = function() {
1589
1645
  });
1590
1646
  initSetupTabs();
1591
1647
  initProviderPicker();
1648
+ initProviderModelPreferenceTracking();
1592
1649
  initPcardAccordion();
1593
1650
  // Inject the brand-mark SVG for each provider card (anthropic / openai /
1594
1651
  // google / ollama / mlx). The card headers carry data-pcard-header="<key>";
@@ -1792,21 +1849,8 @@ async function loadProviderCardStates() {
1792
1849
  }
1793
1850
  }
1794
1851
  // Pre-fill model dropdown if backend has a per-provider model
1795
- if (p.model) {
1796
- var sel = document.getElementById(_selectIdFor(p.type));
1797
- if (sel) {
1798
- var found = false;
1799
- for (var j = 0; j < sel.options.length; j++) {
1800
- if (sel.options[j].value === p.model) { sel.value = p.model; found = true; break; }
1801
- }
1802
- if (!found) {
1803
- var opt = document.createElement('option');
1804
- opt.value = p.model; opt.textContent = p.model + ' (custom)';
1805
- sel.insertBefore(opt, sel.firstChild);
1806
- sel.value = p.model;
1807
- }
1808
- }
1809
- }
1852
+ if (p.model) _applyProviderModelSelection(p.type, p.model);
1853
+ else _rememberProviderModel(p.type, '');
1810
1854
  }
1811
1855
  // Conditional Devbox row: only show under Anthropic if the file exists
1812
1856
  // server-side. We probe via a lightweight HEAD-style check: ask the
@@ -340,32 +340,58 @@ function _mergeConsecutiveToolGroups(container, newEl) {
340
340
  const newItems = newEl.querySelector('.thought-group-items');
341
341
  if (!targetItems || !newItems) return false;
342
342
  while (newItems.firstChild) targetItems.appendChild(newItems.firstChild);
343
- // Update the count
344
- const countEl = lastChild.querySelector('.thought-group-count');
345
- if (countEl) {
346
- const stepCount = targetItems.children.length;
347
- countEl.textContent = '— ' + stepCount + ' step' + (stepCount === 1 ? '' : 's');
348
- }
349
- // Extend the time range to first–last
343
+ // Extend the time range to first–last, then let the shared renderer
344
+ // recompute count + preview. Keeping all item parentUuids on the nested
345
+ // rows prevents a later update from replacing the whole merged group.
350
346
  const newLast = newEl.dataset.lastTime || '';
351
347
  if (newLast) lastChild.dataset.lastTime = newLast;
352
- const tEl = lastChild.querySelector('.thought-group-time');
353
- if (tEl) {
354
- const f = lastChild.dataset.firstTime || '';
355
- const l = lastChild.dataset.lastTime || '';
356
- tEl.textContent = (f && l && f !== l) ? f + ' – ' + l : (l || f);
348
+ if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
349
+ MR.refreshConversationActivityGroup(lastChild);
350
+ }
351
+ return true;
352
+ }
353
+
354
+ function _assignConversationParentUuid(el, parentUuid) {
355
+ if (!el || !parentUuid) return;
356
+ if (el.classList && el.classList.contains('conv-tool-group')) {
357
+ const items = el.querySelector('.thought-group-items');
358
+ const row = items && (items.lastElementChild || (items.children && items.children[items.children.length - 1]));
359
+ if (row && row.dataset && !row.dataset.parentUuid) row.dataset.parentUuid = parentUuid;
360
+ } else if (el.dataset) {
361
+ el.dataset.parentUuid = parentUuid;
362
+ }
363
+ }
364
+
365
+ function _replaceConversationParentEvent(existing, newEl) {
366
+ if (!existing) return false;
367
+ const existingGroup = existing.closest ? existing.closest('.conv-tool-group') : null;
368
+ if (existingGroup && newEl && newEl.classList && newEl.classList.contains('conv-tool-group')) {
369
+ const targetRow = existing.closest('.tool-activity-item') || existing;
370
+ const targetParent = targetRow.parentNode;
371
+ const newItems = newEl.querySelector('.thought-group-items');
372
+ if (!targetParent || !newItems) return false;
373
+ const rows = Array.from(newItems.children || []);
374
+ for (const row of rows) targetParent.insertBefore(row, targetRow);
375
+ targetParent.removeChild(targetRow);
376
+ if (newEl.dataset.lastTime) existingGroup.dataset.lastTime = newEl.dataset.lastTime;
377
+ if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
378
+ MR.refreshConversationActivityGroup(existingGroup);
379
+ }
380
+ return true;
357
381
  }
358
- // Preview rule (matches Review's renderGroup): keep the FIRST non-empty
359
- // preview as the group summary. Once a narration line ("Let me read…")
360
- // anchors the preview, later tool-only steps don't overwrite it.
361
- // Only update if the existing preview is empty AND the incoming has text.
362
- const previewEl = lastChild.querySelector('.thought-group-preview');
363
- const newPreviewEl = newEl.querySelector('.thought-group-preview');
364
- if (previewEl && newPreviewEl && newPreviewEl.textContent && !previewEl.textContent) {
365
- previewEl.textContent = newPreviewEl.textContent;
382
+ if (existingGroup && !newEl) {
383
+ const targetRow = existing.closest('.tool-activity-item') || existing;
384
+ const targetParent = targetRow.parentNode;
385
+ if (targetParent) targetParent.removeChild(targetRow);
386
+ const remaining = existingGroup.querySelector('.thought-group-items')?.children?.length || 0;
387
+ if (remaining === 0) existingGroup.remove();
388
+ else if (typeof MR !== 'undefined' && typeof MR.refreshConversationActivityGroup === 'function') {
389
+ MR.refreshConversationActivityGroup(existingGroup);
390
+ }
391
+ return true;
366
392
  }
367
- // Carry the latest parentUuid forward so dedup still finds this group.
368
- if (newEl.dataset.parentUuid) lastChild.dataset.parentUuid = newEl.dataset.parentUuid;
393
+ if (newEl) existing.replaceWith(newEl);
394
+ else existing.remove();
369
395
  return true;
370
396
  }
371
397
 
@@ -379,6 +405,7 @@ function populateConversationView(container, events) {
379
405
  for (const evt of events) {
380
406
  const el = renderConversationEvent(evt);
381
407
  if (!el) continue;
408
+ if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
382
409
  if (_mergeConsecutiveToolGroups(container, el)) continue;
383
410
  container.appendChild(el);
384
411
  }
@@ -671,10 +698,10 @@ function _applyStreamEvent(sessionId, container, msg) {
671
698
  if (existing) {
672
699
  const newEl = renderConversationEvent(msg);
673
700
  if (newEl) {
674
- newEl.dataset.parentUuid = parentUuid;
675
- existing.replaceWith(newEl);
701
+ _assignConversationParentUuid(newEl, parentUuid);
702
+ _replaceConversationParentEvent(existing, newEl);
676
703
  } else {
677
- existing.remove(); // Event became empty after update
704
+ _replaceConversationParentEvent(existing, null); // Event became empty after update
678
705
  }
679
706
  return;
680
707
  }
@@ -690,8 +717,8 @@ function _applyStreamEvent(sessionId, container, msg) {
690
717
  if (existing) {
691
718
  const newEl = renderConversationEvent(msg);
692
719
  if (newEl) {
693
- newEl.dataset.parentUuid = parentUuid;
694
- existing.replaceWith(newEl);
720
+ _assignConversationParentUuid(newEl, parentUuid);
721
+ _replaceConversationParentEvent(existing, newEl);
695
722
  }
696
723
  return;
697
724
  }
@@ -701,7 +728,7 @@ function _applyStreamEvent(sessionId, container, msg) {
701
728
  if (!el) return; // Skip empty events
702
729
  _removeConversationState(container);
703
730
  if (parentUuid) {
704
- el.dataset.parentUuid = parentUuid;
731
+ _assignConversationParentUuid(el, parentUuid);
705
732
  if (seen) seen.add(parentUuid);
706
733
  }
707
734
  // If this is a tool-only thought-group AND the last rendered child is also
@@ -920,7 +947,8 @@ async function _loadOlder(sessionId, convView) {
920
947
  for (const evt of events) {
921
948
  const el = renderConversationEvent(evt);
922
949
  if (el) {
923
- if (evt.data?.parentUuid) el.dataset.parentUuid = evt.data.parentUuid;
950
+ if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
951
+ if (_mergeConsecutiveToolGroups(frag, el)) continue;
924
952
  frag.appendChild(el);
925
953
  }
926
954
  }
@@ -957,18 +985,6 @@ async function _primeConversationView(sessionId, convView) {
957
985
  const events = _messagesToEvents(page.messages);
958
986
  populateConversationView(convView, events);
959
987
 
960
- // Assign data-parent-uuid on each primed row so live stream-events
961
- // that cover the same turn can find + replace instead of duplicate.
962
- // populateConversationView appends children in order; we walk in
963
- // lockstep skipping events that rendered to null (empty).
964
- let domIdx = 0;
965
- for (const e of events) {
966
- const child = convView.children[domIdx];
967
- if (!child) break;
968
- if (e.data?.parentUuid) child.dataset.parentUuid = e.data.parentUuid;
969
- domIdx++;
970
- }
971
-
972
988
  // Seed the parentUuid dedup set from the primed history so live
973
989
  // stream-events that cover the same turns don't re-append.
974
990
  const seen = new Set();
@@ -1011,24 +1027,62 @@ function _resetPrimingState(sessionId) {
1011
1027
 
1012
1028
  // --- Tooltip hover binding ---
1013
1029
 
1030
+ function _streamTooltipContains(parent, child) {
1031
+ if (!parent || !child) return false;
1032
+ if (typeof parent.contains === 'function') return parent.contains(child);
1033
+ for (let node = child; node; node = node.parentNode) {
1034
+ if (node === parent) return true;
1035
+ }
1036
+ return false;
1037
+ }
1038
+
1039
+ function _streamTooltipClosestSessionItem(target, root) {
1040
+ let item = null;
1041
+ if (target && typeof target.closest === 'function') {
1042
+ item = target.closest('.session-item');
1043
+ } else {
1044
+ for (let node = target; node; node = node.parentNode) {
1045
+ const classes = String(node.className || '').split(/\s+/);
1046
+ if (classes.includes('session-item')) {
1047
+ item = node;
1048
+ break;
1049
+ }
1050
+ }
1051
+ }
1052
+ if (!item) return null;
1053
+ if (root && item !== root && !_streamTooltipContains(root, item)) return null;
1054
+ return item;
1055
+ }
1056
+
1057
+ function _scheduleStreamTooltip(item) {
1058
+ const id = item?.dataset?.sessionId;
1059
+ if (!id) return;
1060
+ _streamState.tooltipTimer = setTimeout(() => showStreamTooltip(id, item), 500);
1061
+ }
1062
+
1063
+ function _handleStreamTooltipMouseOver(e, list) {
1064
+ const item = _streamTooltipClosestSessionItem(e.target, list);
1065
+ if (!item) return;
1066
+ if (e.relatedTarget && _streamTooltipContains(item, e.relatedTarget)) return;
1067
+ hideStreamTooltip();
1068
+ _scheduleStreamTooltip(item);
1069
+ }
1070
+
1071
+ function _handleStreamTooltipMouseOut(e, list) {
1072
+ const item = _streamTooltipClosestSessionItem(e.target, list);
1073
+ if (!item) return;
1074
+ if (e.relatedTarget && _streamTooltipContains(item, e.relatedTarget)) return;
1075
+ hideStreamTooltip();
1076
+ }
1077
+
1014
1078
  function bindStreamTooltips() {
1015
1079
  const list = document.getElementById('session-list');
1016
1080
  if (!list) return;
1081
+ if (list.__streamTooltipBound) return;
1082
+ list.__streamTooltipBound = true;
1017
1083
 
1018
- list.addEventListener('mouseenter', (e) => {
1019
- // Always cancel pending tooltip on any enter (prevents stale timers between items)
1020
- hideStreamTooltip();
1021
- const item = e.target.closest('.session-item');
1022
- if (!item) return;
1023
- const id = item.dataset.sessionId;
1024
- if (!id) return;
1025
- _streamState.tooltipTimer = setTimeout(() => showStreamTooltip(id, item), 500);
1026
- }, true);
1027
-
1028
- list.addEventListener('mouseleave', () => {
1029
- // Always dismiss — don't gate on closest('.session-item')
1030
- hideStreamTooltip();
1031
- }, true);
1084
+ list.addEventListener('mouseover', (e) => _handleStreamTooltipMouseOver(e, list));
1085
+ list.addEventListener('mouseout', (e) => _handleStreamTooltipMouseOut(e, list));
1032
1086
  }
1033
1087
 
1034
1088
  // --- Init ---
@@ -1061,6 +1115,12 @@ if (typeof module !== 'undefined' && module.exports) {
1061
1115
  _fetchConversationPage,
1062
1116
  renderConversationEvent,
1063
1117
  populateConversationView,
1118
+ _mergeConsecutiveToolGroups,
1064
1119
  _streamState,
1120
+ bindStreamTooltips,
1121
+ _handleStreamTooltipMouseOver,
1122
+ _handleStreamTooltipMouseOut,
1123
+ _streamTooltipClosestSessionItem,
1124
+ _streamTooltipContains,
1065
1125
  };
1066
1126
  }
@@ -435,6 +435,25 @@ window.WalleSession = (function() {
435
435
  }
436
436
 
437
437
  // ---------- sendMessage ----------
438
+ function resolveContextSessionId(id) {
439
+ var preferred = state.lastActiveWorkSessionId || state._savedActiveSession || '';
440
+ if (preferred && preferred !== id) {
441
+ var preferredSession = state.sessions.get(preferred);
442
+ if (preferredSession && preferredSession.meta?.type !== 'walle') return preferred;
443
+ }
444
+ var idx = state.tabOrder.indexOf(id);
445
+ var order = idx >= 0
446
+ ? state.tabOrder.slice(0, idx).reverse().concat(state.tabOrder.slice(idx + 1).reverse())
447
+ : state.tabOrder.slice().reverse();
448
+ for (var i = 0; i < order.length; i++) {
449
+ var sid = order[i];
450
+ if (sid === id) continue;
451
+ var s = state.sessions.get(sid);
452
+ if (s && s.meta?.type !== 'walle') return sid;
453
+ }
454
+ return '';
455
+ }
456
+
438
457
  function sendMessage(id) {
439
458
  var container = document.getElementById('walle-session-' + id);
440
459
  if (!container) return;
@@ -458,7 +477,7 @@ window.WalleSession = (function() {
458
477
 
459
478
  // Send via WebSocket (include model if user selected one)
460
479
  var model = ws.selectedModel || '';
461
- send({ type: 'walle-message', id: id, text: text, model: model });
480
+ send({ type: 'walle-message', id: id, text: text, model: model, contextSessionId: resolveContextSessionId(id) });
462
481
 
463
482
  // Clear input
464
483
  textarea.value = '';
@@ -632,6 +651,67 @@ window.WalleSession = (function() {
632
651
  return text;
633
652
  }
634
653
 
654
+ function normalizeProviderIssue(source) {
655
+ if (!source || typeof source !== 'object') return null;
656
+ var issue = source.providerError || source.provider_error || source;
657
+ if (!issue || typeof issue !== 'object') return null;
658
+ if (!issue.title && !issue.rawMessage && !issue.userMessage && issue.code !== 'AI_PROVIDER_ERROR') return null;
659
+ return {
660
+ title: issue.title || 'AI provider failed',
661
+ message: issue.userMessage || issue.message || source.message || source.error || 'Wall-E could not get a response from the configured AI provider.',
662
+ rawMessage: issue.rawMessage || '',
663
+ provider: issue.provider || '',
664
+ model: issue.model || '',
665
+ status: issue.status || '',
666
+ retryAfter: issue.retryAfter || '',
667
+ actionUrl: issue.actionUrl || issue.action_url || '/setup.html',
668
+ actionLabel: issue.actionLabel || 'Open Setup'
669
+ };
670
+ }
671
+
672
+ function appendProviderIssueNotice(notice, issue) {
673
+ notice.className += ' provider';
674
+
675
+ var title = document.createElement('div');
676
+ title.className = 'walle-error-title';
677
+ title.textContent = issue.title;
678
+ notice.appendChild(title);
679
+
680
+ var body = document.createElement('div');
681
+ body.className = 'walle-error-body';
682
+ body.textContent = issue.message;
683
+ notice.appendChild(body);
684
+
685
+ var detailLines = [];
686
+ if (issue.status) detailLines.push('HTTP/status: ' + issue.status);
687
+ if (issue.provider) detailLines.push('Provider: ' + issue.provider);
688
+ if (issue.model) detailLines.push('Model: ' + issue.model);
689
+ if (issue.retryAfter) detailLines.push('Retry after: ' + issue.retryAfter);
690
+ if (issue.rawMessage) detailLines.push('', issue.rawMessage);
691
+ if (detailLines.length) {
692
+ var details = document.createElement('details');
693
+ details.className = 'walle-error-details';
694
+ var summary = document.createElement('summary');
695
+ summary.textContent = 'Provider details';
696
+ details.appendChild(summary);
697
+ var pre = document.createElement('pre');
698
+ pre.textContent = detailLines.join('\n').trim();
699
+ details.appendChild(pre);
700
+ notice.appendChild(details);
701
+ }
702
+
703
+ if (issue.actionUrl && /^\/setup/.test(issue.actionUrl) && typeof navTo === 'function') {
704
+ var actions = document.createElement('div');
705
+ actions.className = 'walle-error-actions';
706
+ var btn = document.createElement('button');
707
+ btn.className = 'walle-session-action-btn primary';
708
+ btn.textContent = issue.actionLabel || 'Open Setup';
709
+ btn.onclick = function() { navTo('setup'); };
710
+ actions.appendChild(btn);
711
+ notice.appendChild(actions);
712
+ }
713
+ }
714
+
635
715
  function handleError(msg) {
636
716
  var id = msg.id;
637
717
  var messagesArea = document.getElementById('walle-messages-' + id);
@@ -647,7 +727,9 @@ window.WalleSession = (function() {
647
727
 
648
728
  var notice = document.createElement('div');
649
729
  notice.className = 'walle-error-notice';
650
- notice.textContent = formatWalleError(msg.error);
730
+ var providerIssue = normalizeProviderIssue(msg.providerError ? { providerError: msg.providerError } : null);
731
+ if (providerIssue) appendProviderIssueNotice(notice, providerIssue);
732
+ else notice.textContent = formatWalleError(msg.error);
651
733
  messagesArea.appendChild(notice);
652
734
  scrollToBottom(messagesArea);
653
735
  updateSendButton(id, false);