create-walle 0.9.13 → 0.9.15

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 (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -14,6 +14,11 @@ const _streamState = {
14
14
  activeView: new Map(),
15
15
  tooltipEl: null,
16
16
  tooltipTimer: null,
17
+ tooltipAnchorEl: null,
18
+ tooltipRefreshTimer: null,
19
+ tooltipRefreshDebounceTimer: null,
20
+ _tooltipActiveSessionId: null,
21
+ _tooltipPendingSessionId: null,
17
22
  _tooltipReqId: 0,
18
23
  // Per-session priming bookkeeping.
19
24
  // _primed: sessionId → true once _primeConversationView() has rendered
@@ -22,11 +27,20 @@ const _streamState = {
22
27
  // _priming: sessionId → in-flight Promise while the HTTP fetch is
23
28
  // running — prevents duplicate primes when stream-init fires twice
24
29
  // (e.g. reconnect).
30
+ // _primedTarget/_primingTarget: sessionId → conversation DOM node that
31
+ // owns the completed/in-flight prime. Tab activation can recreate the
32
+ // node while keeping the same sessionId; stale state must not make the
33
+ // new empty node skip its HTTP prime.
34
+ // _primeToken: sessionId → monotonic generation that invalidates older
35
+ // in-flight primes when the view is reset or recreated.
25
36
  // _pendingEvents: sessionId → [stream-event...] buffered during prime.
26
37
  // _parentUuidSeen: sessionId → Set of parentUuids already rendered,
27
38
  // used to dedup stream-events that overlap with primed history.
28
39
  _primed: new Map(),
40
+ _primedTarget: new Map(),
29
41
  _priming: new Map(),
42
+ _primingTarget: new Map(),
43
+ _primeToken: new Map(),
30
44
  _pendingEvents: new Map(),
31
45
  _parentUuidSeen: new Map(),
32
46
  };
@@ -41,9 +55,13 @@ function initStreamTooltip() {
41
55
  background: var(--surface-2, #1a1a2e); color: var(--fg, #e0e0e0);
42
56
  border: 1px solid var(--border, #333); border-radius: 8px;
43
57
  width: min(420px, calc(100vw - 24px)); max-height: min(520px, calc(100vh - 16px));
44
- overflow: hidden; font-size: 12px; line-height: 1.45; pointer-events: none;
58
+ overflow: hidden; font-size: 12px; line-height: 1.45; pointer-events: auto;
59
+ user-select: text; cursor: text;
45
60
  box-shadow: 0 10px 28px rgba(0,0,0,0.42), 0 0 0 1px rgba(255,255,255,0.02) inset;
46
61
  `;
62
+ tooltip.tabIndex = -1;
63
+ tooltip.setAttribute('role', 'dialog');
64
+ tooltip.setAttribute('aria-label', 'Session summary');
47
65
  document.body.appendChild(tooltip);
48
66
  _streamState.tooltipEl = tooltip;
49
67
  }
@@ -79,6 +97,7 @@ function _tooltipStatusLabel(status) {
79
97
  function _tooltipSourceLabel(source) {
80
98
  const key = _tooltipText(source).toLowerCase();
81
99
  if (key === 'ai-summary') return 'AI summary';
100
+ if (key === 'latest-prompt') return 'Latest prompt';
82
101
  if (key === 'prompt-fallback') return 'Prompt fallback';
83
102
  if (key === 'title-fallback') return 'Title fallback';
84
103
  if (key === 'missing') return 'Intent missing';
@@ -95,6 +114,25 @@ function _tooltipPhaseLabel(phase) {
95
114
  return key ? _tooltipStatusLabel(key) : 'Progress';
96
115
  }
97
116
 
117
+ function _tooltipFreshnessLabel(freshness) {
118
+ const key = _tooltipText(freshness).toLowerCase();
119
+ if (key === 'fresh') return 'Fresh';
120
+ if (key === 'updating') return 'Updating';
121
+ if (key === 'stale') return 'Stale AI';
122
+ if (key === 'fallback') return 'No AI';
123
+ if (key === 'rejected') return 'Rejected AI';
124
+ if (key === 'missing') return 'Missing';
125
+ return key ? _tooltipStatusLabel(key) : 'Freshness';
126
+ }
127
+
128
+ function _tooltipFreshnessTone(freshness) {
129
+ const key = _tooltipText(freshness).toLowerCase();
130
+ if (key === 'fresh') return 'intent';
131
+ if (key === 'updating') return 'progress';
132
+ if (key === 'stale' || key === 'rejected') return 'fallback';
133
+ return 'idle';
134
+ }
135
+
98
136
  function _tooltipAgentLabel(session, anchorEl) {
99
137
  const agent = _tooltipText(anchorEl?.dataset?.agent).toLowerCase();
100
138
  if (agent) {
@@ -192,10 +230,96 @@ function _tooltipAppendProgress(container, progress) {
192
230
  container.appendChild(_tooltipSection('Progress', body));
193
231
  }
194
232
 
195
- async function showStreamTooltip(sessionId, anchorEl) {
196
- if (_streamState.tooltipTimer) clearTimeout(_streamState.tooltipTimer);
233
+ function _tooltipCurrentTask(summary, session) {
234
+ const currentTask = summary?.currentTask || null;
235
+ if (currentTask && (currentTask.text || currentTask.source)) return currentTask;
236
+ const intent = summary?.intent || {};
237
+ const displayText = _tooltipText(intent.text || summary?.summary || summary?.displayPrompt || summary?.lastPrompt || session?.meta?.label);
238
+ return {
239
+ text: displayText || null,
240
+ source: intent.source || (summary?.summary ? 'ai-summary' : 'prompt-fallback'),
241
+ confidence: intent.confidence || (summary?.summary ? 'high' : 'low'),
242
+ freshness: intent.freshness || (summary?.summary ? 'fresh' : 'fallback'),
243
+ timestamp: intent.timestamp || 0,
244
+ updatedAt: intent.updatedAt || 0,
245
+ prompt: intent.prompt || summary?.displayPrompt || summary?.lastPrompt || null,
246
+ fullPrompt: intent.fullPrompt || summary?.displayPrompt || summary?.lastPrompt || null,
247
+ };
248
+ }
249
+
250
+ function _tooltipAppendAiSummary(container, aiSummary, currentTask) {
251
+ const aiText = _tooltipText(aiSummary?.text);
252
+ if (!aiText) return;
253
+ if (_tooltipText(currentTask?.source) === 'ai-summary' && aiText === _tooltipText(currentTask?.text)) return;
254
+
255
+ const body = document.createElement('div');
256
+ const row = document.createElement('div');
257
+ row.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:5px;';
258
+ row.appendChild(_tooltipPill(_tooltipFreshnessLabel(aiSummary.status || aiSummary.freshness), _tooltipFreshnessTone(aiSummary.status || aiSummary.freshness)));
259
+ if (aiSummary.model) row.appendChild(_tooltipPill(_tooltipCap(aiSummary.model, 24), 'idle'));
260
+ const age = _tooltipAge(aiSummary.updatedAt);
261
+ if (age) {
262
+ const ageEl = document.createElement('span');
263
+ ageEl.style.cssText = 'align-self:center;color:var(--fg-dim,#727aa1);font-size:11px;';
264
+ ageEl.textContent = age;
265
+ row.appendChild(ageEl);
266
+ }
267
+ body.appendChild(row);
268
+ body.appendChild(_tooltipTextBlock(aiText, false));
269
+ container.appendChild(_tooltipSection('AI Summary', body));
270
+ }
271
+
272
+ function _resolveStreamTooltipAnchor(sessionId) {
273
+ const current = _streamState.tooltipAnchorEl;
274
+ if (current && typeof current.getBoundingClientRect === 'function' && (current.isConnected !== false)) return current;
275
+ if (typeof document === 'undefined' || !document.querySelector) return current;
276
+ try {
277
+ return document.querySelector(`#session-list .session-item[data-session-id="${CSS.escape(sessionId)}"]`) || current;
278
+ } catch {
279
+ return current;
280
+ }
281
+ }
282
+
283
+ function _clearTooltipRefreshTimers() {
284
+ if (_streamState.tooltipRefreshTimer) {
285
+ clearTimeout(_streamState.tooltipRefreshTimer);
286
+ _streamState.tooltipRefreshTimer = null;
287
+ }
288
+ if (_streamState.tooltipRefreshDebounceTimer) {
289
+ clearTimeout(_streamState.tooltipRefreshDebounceTimer);
290
+ _streamState.tooltipRefreshDebounceTimer = null;
291
+ }
292
+ }
293
+
294
+ function _scheduleVisibleStreamTooltipRefresh() {
295
+ if (_streamState.tooltipRefreshTimer) clearTimeout(_streamState.tooltipRefreshTimer);
296
+ _streamState.tooltipRefreshTimer = null;
297
+ if (!_streamTooltipVisible() || !_streamState._tooltipActiveSessionId) return;
298
+ _streamState.tooltipRefreshTimer = setTimeout(() => {
299
+ _streamState.tooltipRefreshTimer = null;
300
+ if (!_streamTooltipVisible() || !_streamState._tooltipActiveSessionId) return;
301
+ const id = _streamState._tooltipActiveSessionId;
302
+ showStreamTooltip(id, _resolveStreamTooltipAnchor(id), { refresh: true });
303
+ }, 10000);
304
+ if (typeof _streamState.tooltipRefreshTimer.unref === 'function') _streamState.tooltipRefreshTimer.unref();
305
+ }
306
+
307
+ function _refreshVisibleStreamTooltip(delayMs = 250) {
308
+ if (!_streamTooltipVisible() || !_streamState._tooltipActiveSessionId) return;
309
+ if (_streamState.tooltipRefreshDebounceTimer) clearTimeout(_streamState.tooltipRefreshDebounceTimer);
310
+ _streamState.tooltipRefreshDebounceTimer = setTimeout(() => {
311
+ _streamState.tooltipRefreshDebounceTimer = null;
312
+ if (!_streamTooltipVisible() || !_streamState._tooltipActiveSessionId) return;
313
+ const id = _streamState._tooltipActiveSessionId;
314
+ showStreamTooltip(id, _resolveStreamTooltipAnchor(id), { refresh: true });
315
+ }, delayMs);
316
+ }
317
+
318
+ async function showStreamTooltip(sessionId, anchorEl, opts = {}) {
319
+ _clearPendingStreamTooltip(false);
197
320
  const tooltip = _streamState.tooltipEl;
198
321
  if (!tooltip) return;
322
+ if (anchorEl) _streamState.tooltipAnchorEl = anchorEl;
199
323
 
200
324
  // Tag this request so we can detect staleness after await
201
325
  const requestId = ++_streamState._tooltipReqId;
@@ -239,14 +363,15 @@ async function showStreamTooltip(sessionId, anchorEl) {
239
363
  header.appendChild(_tooltipPill(_tooltipStatusLabel(status), statusTone));
240
364
  content.appendChild(header);
241
365
 
242
- const intent = summary.intent || {};
243
- const displayText = _tooltipText(intent.text || summary.summary || summary.displayPrompt || summary.lastPrompt || session?.meta?.label);
366
+ const currentTask = _tooltipCurrentTask(summary, session);
367
+ const displayText = _tooltipText(currentTask.text || summary.displayPrompt || summary.lastPrompt || session?.meta?.label);
244
368
  const intentBody = document.createElement('div');
245
369
  const sourceRow = document.createElement('div');
246
370
  sourceRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-bottom:5px;';
247
- sourceRow.appendChild(_tooltipPill(_tooltipSourceLabel(intent.source), intent.source === 'ai-summary' ? 'intent' : 'fallback'));
248
- if (intent.confidence) sourceRow.appendChild(_tooltipPill(_tooltipStatusLabel(intent.confidence), 'idle'));
249
- const intentAge = _tooltipAge(intent.updatedAt || intent.timestamp);
371
+ sourceRow.appendChild(_tooltipPill(_tooltipSourceLabel(currentTask.source), currentTask.source === 'ai-summary' ? 'intent' : 'fallback'));
372
+ if (currentTask.freshness) sourceRow.appendChild(_tooltipPill(_tooltipFreshnessLabel(currentTask.freshness), _tooltipFreshnessTone(currentTask.freshness)));
373
+ if (currentTask.confidence) sourceRow.appendChild(_tooltipPill(_tooltipStatusLabel(currentTask.confidence), 'idle'));
374
+ const intentAge = _tooltipAge(currentTask.updatedAt || currentTask.timestamp || currentTask.promptTimestamp);
250
375
  if (intentAge) {
251
376
  const ageEl = document.createElement('span');
252
377
  ageEl.style.cssText = 'align-self:center;color:var(--fg-dim,#727aa1);font-size:11px;';
@@ -255,7 +380,9 @@ async function showStreamTooltip(sessionId, anchorEl) {
255
380
  }
256
381
  intentBody.appendChild(sourceRow);
257
382
  intentBody.appendChild(_tooltipTextBlock(displayText || 'No intent captured yet.', !displayText));
258
- content.appendChild(_tooltipSection('Intent', intentBody));
383
+ content.appendChild(_tooltipSection('Current Task', intentBody));
384
+
385
+ _tooltipAppendAiSummary(content, summary.aiSummary, currentTask);
259
386
 
260
387
  _tooltipAppendProgress(content, summary.progress);
261
388
 
@@ -273,32 +400,47 @@ async function showStreamTooltip(sessionId, anchorEl) {
273
400
 
274
401
  tooltip.appendChild(content);
275
402
 
403
+ tooltip.dataset.sessionId = sessionId;
404
+ _streamState._tooltipActiveSessionId = sessionId;
405
+ _streamState.tooltipAnchorEl = anchorEl || _streamState.tooltipAnchorEl;
276
406
  tooltip.style.display = 'block';
277
407
 
278
- // Position next to anchor (right of sidebar)
279
- const rect = anchorEl.getBoundingClientRect();
280
- const sidebar = document.getElementById('sidebar');
281
- const sidebarRight = sidebar ? sidebar.getBoundingClientRect().right : rect.right;
282
- tooltip.style.left = (sidebarRight + 8) + 'px';
283
- tooltip.style.top = Math.max(8, rect.top - 10) + 'px';
284
-
285
- // Keep within viewport
286
- const tr = tooltip.getBoundingClientRect();
287
- if (tr.right > window.innerWidth - 8) {
288
- tooltip.style.left = (rect.left - tr.width - 8) + 'px';
289
- }
290
- if (tr.bottom > window.innerHeight - 8) {
291
- tooltip.style.top = (window.innerHeight - tr.height - 8) + 'px';
408
+ const anchor = anchorEl || _resolveStreamTooltipAnchor(sessionId);
409
+ if (anchor && typeof anchor.getBoundingClientRect === 'function') {
410
+ // Position next to anchor (right of sidebar)
411
+ const rect = anchor.getBoundingClientRect();
412
+ const sidebar = document.getElementById && document.getElementById('sidebar');
413
+ const sidebarRight = sidebar ? sidebar.getBoundingClientRect().right : rect.right;
414
+ tooltip.style.left = (sidebarRight + 8) + 'px';
415
+ tooltip.style.top = Math.max(8, rect.top - 10) + 'px';
416
+
417
+ // Keep within viewport
418
+ const tr = tooltip.getBoundingClientRect();
419
+ const viewportWidth = Number.isFinite(window.innerWidth) ? window.innerWidth : 1200;
420
+ const viewportHeight = Number.isFinite(window.innerHeight) ? window.innerHeight : 800;
421
+ if (tr.right > viewportWidth - 8) {
422
+ tooltip.style.left = (rect.left - tr.width - 8) + 'px';
423
+ }
424
+ if (tr.bottom > viewportHeight - 8) {
425
+ tooltip.style.top = (viewportHeight - tr.height - 8) + 'px';
426
+ }
292
427
  }
428
+ if (!opts.noRefresh) _scheduleVisibleStreamTooltipRefresh();
293
429
  } catch {
294
430
  hideStreamTooltip();
295
431
  }
296
432
  }
297
433
 
298
434
  function hideStreamTooltip() {
299
- if (_streamState.tooltipTimer) { clearTimeout(_streamState.tooltipTimer); _streamState.tooltipTimer = null; }
435
+ _clearPendingStreamTooltip(false);
436
+ _clearTooltipRefreshTimers();
300
437
  _streamState._tooltipReqId++; // Invalidate any in-flight fetch
301
- if (_streamState.tooltipEl) _streamState.tooltipEl.style.display = 'none';
438
+ _streamState._tooltipActiveSessionId = null;
439
+ _streamState.tooltipAnchorEl = null;
440
+ if (_streamState.tooltipEl) {
441
+ _streamState.tooltipEl.style.display = 'none';
442
+ delete _streamState.tooltipEl.dataset.sessionId;
443
+ }
302
444
  }
303
445
 
304
446
  // --- Conversation View ---
@@ -307,6 +449,7 @@ function createConversationView(sessionId) {
307
449
  const container = document.createElement('div');
308
450
  container.className = 'conversation-view';
309
451
  container.dataset.sessionId = sessionId;
452
+ container.dataset.turnMode = 'conversation';
310
453
  container.style.cssText = `
311
454
  display: none; flex-direction: column; height: 100%;
312
455
  overflow-y: auto; padding: 12px 16px; background: var(--surface-1, #111);
@@ -364,6 +507,11 @@ function _assignConversationParentUuid(el, parentUuid) {
364
507
 
365
508
  function _replaceConversationParentEvent(existing, newEl) {
366
509
  if (!existing) return false;
510
+ if (existing.classList && existing.classList.contains('prompt-turn')) {
511
+ if (newEl) existing.replaceWith(newEl);
512
+ else existing.remove();
513
+ return true;
514
+ }
367
515
  const existingGroup = existing.closest ? existing.closest('.conv-tool-group') : null;
368
516
  if (existingGroup && newEl && newEl.classList && newEl.classList.contains('conv-tool-group')) {
369
517
  const targetRow = existing.closest('.tool-activity-item') || existing;
@@ -395,20 +543,102 @@ function _replaceConversationParentEvent(existing, newEl) {
395
543
  return true;
396
544
  }
397
545
 
546
+ function _isPromptTurnContainer(container) {
547
+ return !!(container && container.dataset && (container.dataset.turnMode === 'conversation' || container.dataset.turnMode === 'review'));
548
+ }
549
+
550
+ function _latestPromptTurn(container) {
551
+ if (!container || !container.children) return null;
552
+ for (let i = container.children.length - 1; i >= 0; i--) {
553
+ const child = container.children[i];
554
+ if (child && child.classList && child.classList.contains('prompt-turn')) return child;
555
+ }
556
+ return null;
557
+ }
558
+
559
+ function _collapsePromptTurns(container) {
560
+ if (!container || !container.children || !MR || typeof MR.setPromptTurnExpanded !== 'function') return;
561
+ for (const child of Array.from(container.children)) {
562
+ if (child && child.classList && child.classList.contains('prompt-turn')) MR.setPromptTurnExpanded(child, false);
563
+ }
564
+ }
565
+
566
+ function _expandLatestPromptTurn(container) {
567
+ const latest = _latestPromptTurn(container);
568
+ if (latest && MR && typeof MR.setPromptTurnExpanded === 'function') MR.setPromptTurnExpanded(latest, true);
569
+ }
570
+
571
+ function _removePromptTurnEmpty(body) {
572
+ if (!body || !body.children) return;
573
+ for (const child of Array.from(body.children)) {
574
+ if (child && child.classList && child.classList.contains('prompt-turn-empty')) child.remove();
575
+ }
576
+ }
577
+
578
+ function _ensureResponseTurn(container, opts) {
579
+ let turn = _latestPromptTurn(container);
580
+ if (turn) return turn;
581
+ turn = MR.createConversationTurn(null, {
582
+ setup: true,
583
+ expanded: !!(opts && opts.expandSetup),
584
+ setupLabel: 'Session setup and context',
585
+ });
586
+ container.appendChild(turn);
587
+ return turn;
588
+ }
589
+
590
+ function _renderConversationNodeForEvent(evt, opts) {
591
+ if (MR && typeof MR.isConversationPromptEvent === 'function' && MR.isConversationPromptEvent(evt)) {
592
+ return MR.createConversationTurn(evt, { expanded: !!(opts && opts.expandPrompt) });
593
+ }
594
+ return renderConversationEvent(evt);
595
+ }
596
+
597
+ function _appendConversationEventToTurns(container, evt, opts) {
598
+ opts = opts || {};
599
+ if (!container || !MR || typeof MR.isConversationPromptEvent !== 'function') return null;
600
+ if (MR.isConversationPromptEvent(evt)) {
601
+ if (opts.collapseExisting) _collapsePromptTurns(container);
602
+ const turn = MR.createConversationTurn(evt, { expanded: !!opts.expandPrompt });
603
+ _removeConversationState(container);
604
+ container.appendChild(turn);
605
+ if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
606
+ return turn;
607
+ }
608
+
609
+ const el = renderConversationEvent(evt);
610
+ if (!el) return null;
611
+ _removeConversationState(container);
612
+ const turn = _ensureResponseTurn(container, opts);
613
+ const body = MR.getPromptTurnBody(turn);
614
+ if (!body) return null;
615
+ _removePromptTurnEmpty(body);
616
+ if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
617
+ if (!_mergeConsecutiveToolGroups(body, el)) body.appendChild(el);
618
+ if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
619
+ return el;
620
+ }
621
+
398
622
  function populateConversationView(container, events) {
399
623
  container.textContent = '';
624
+ if (container.dataset) container.dataset.turnMode = container.dataset.turnMode || 'conversation';
400
625
  if (!events || events.length === 0) {
401
626
  _renderConversationState(container, 'No transcript messages found for this session.');
402
627
  container.scrollTop = container.scrollHeight;
403
628
  return;
404
629
  }
405
630
  for (const evt of events) {
631
+ if (_isPromptTurnContainer(container)) {
632
+ _appendConversationEventToTurns(container, evt, { expandPrompt: false, collapseExisting: false, expandSetup: false });
633
+ continue;
634
+ }
406
635
  const el = renderConversationEvent(evt);
407
636
  if (!el) continue;
408
637
  if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
409
638
  if (_mergeConsecutiveToolGroups(container, el)) continue;
410
639
  container.appendChild(el);
411
640
  }
641
+ if (_isPromptTurnContainer(container)) _expandLatestPromptTurn(container);
412
642
  container.scrollTop = container.scrollHeight;
413
643
  }
414
644
 
@@ -442,6 +672,13 @@ function handleStreamMessage(msg) {
442
672
  _primeConversationView(_domId, container);
443
673
  }
444
674
  } else if (msg.type === 'stream-event') {
675
+ const eventType = msg.data?.type || msg.role || msg.eventType || '';
676
+ if (eventType === 'user' && typeof window !== 'undefined' && typeof window._ctmRecordLivePromptPreview === 'function') {
677
+ window._ctmRecordLivePromptPreview(_domId, msg.data?.text || msg.text || '');
678
+ }
679
+ if (_streamState._tooltipActiveSessionId === _domId && ['user', 'assistant', 'summary'].includes(eventType)) {
680
+ _refreshVisibleStreamTooltip(eventType === 'summary' ? 50 : 250);
681
+ }
445
682
  const container = document.querySelector(`.conversation-view[data-session-id="${CSS.escape(_domId)}"]`);
446
683
  if (container) {
447
684
  // If we haven't finished priming yet, queue the event and replay it later.
@@ -465,7 +702,11 @@ function handleStreamMessage(msg) {
465
702
  if (reviewEl) _applyStreamEvent('__review_' + _domId, reviewEl, msg);
466
703
  }
467
704
  } else if (msg.type === 'stream-status') {
468
- if (applyStreamStatus(msg)) queueStreamStatusRender();
705
+ if (applyStreamStatus(msg)) {
706
+ queueStreamStatusRender();
707
+ const ctmId = msg.ctmSessionId || msg.sessionId;
708
+ if (_streamState._tooltipActiveSessionId === ctmId) _refreshVisibleStreamTooltip(400);
709
+ }
469
710
  }
470
711
  }
471
712
 
@@ -486,6 +727,7 @@ function applyStreamStatus(msg) {
486
727
  if (msg.lastActivity && s.meta) {
487
728
  s.meta.lastActivity = msg.lastActivity;
488
729
  }
730
+ if (changed && typeof scheduleStandupRefresh === 'function') scheduleStandupRefresh();
489
731
  return changed;
490
732
  }
491
733
 
@@ -668,14 +910,15 @@ function setSessionView(sessionId, view) {
668
910
  if (view === 'conversation') {
669
911
  if (xtermEl) xtermEl.style.display = 'none';
670
912
  if (convView) convView.style.display = 'flex';
913
+ if (typeof _renderCodexFinalPanel === 'function') _renderCodexFinalPanel(sessionId);
671
914
  if (window._ws) subscribeToStream(window._ws, sessionId);
672
- // Fallback: if stream returns empty or takes too long, fetch from HTTP API
673
- if (convView && !convView.children.length) {
674
- _primeConversationView(sessionId, convView);
675
- }
915
+ // Prime immediately. _primeConversationView is idempotent for the current
916
+ // DOM node, but will re-prime if tab activation recreated an empty view.
917
+ if (convView) _primeConversationView(sessionId, convView);
676
918
  } else {
677
919
  if (xtermEl) xtermEl.style.display = '';
678
920
  if (convView) convView.style.display = 'none';
921
+ if (typeof _renderCodexFinalPanel === 'function') _renderCodexFinalPanel(sessionId);
679
922
  if (window._ws) unsubscribeFromStream(window._ws, sessionId);
680
923
  }
681
924
  }
@@ -690,13 +933,20 @@ function setSessionView(sessionId, view) {
690
933
  // rendered) replace instead of duplicate.
691
934
  function _applyStreamEvent(sessionId, container, msg) {
692
935
  const parentUuid = msg.data?.parentUuid;
936
+ let seen = _streamState._parentUuidSeen.get(sessionId);
937
+ if (!seen) {
938
+ seen = new Set();
939
+ _streamState._parentUuidSeen.set(sessionId, seen);
940
+ }
693
941
 
694
942
  // Explicit update (server signaled _update=true): replace the row with
695
943
  // this parentUuid.
696
944
  if (msg._update && parentUuid) {
697
945
  const existing = container.querySelector(`[data-parent-uuid="${CSS.escape(parentUuid)}"]`);
698
946
  if (existing) {
699
- const newEl = renderConversationEvent(msg);
947
+ const newEl = _isPromptTurnContainer(container)
948
+ ? _renderConversationNodeForEvent(msg, { expandPrompt: true })
949
+ : renderConversationEvent(msg);
700
950
  if (newEl) {
701
951
  _assignConversationParentUuid(newEl, parentUuid);
702
952
  _replaceConversationParentEvent(existing, newEl);
@@ -711,11 +961,12 @@ function _applyStreamEvent(sessionId, container, msg) {
711
961
  // (either from priming or an earlier stream-event), treat the new one as
712
962
  // a replace rather than an append. Claude Code sends the same parentUuid
713
963
  // for streaming chunks of one assistant turn.
714
- const seen = _streamState._parentUuidSeen.get(sessionId);
715
964
  if (parentUuid && seen && seen.has(parentUuid)) {
716
965
  const existing = container.querySelector(`[data-parent-uuid="${CSS.escape(parentUuid)}"]`);
717
966
  if (existing) {
718
- const newEl = renderConversationEvent(msg);
967
+ const newEl = _isPromptTurnContainer(container)
968
+ ? _renderConversationNodeForEvent(msg, { expandPrompt: true })
969
+ : renderConversationEvent(msg);
719
970
  if (newEl) {
720
971
  _assignConversationParentUuid(newEl, parentUuid);
721
972
  _replaceConversationParentEvent(existing, newEl);
@@ -723,10 +974,25 @@ function _applyStreamEvent(sessionId, container, msg) {
723
974
  return;
724
975
  }
725
976
  }
977
+ if (parentUuid && !seen.has(parentUuid)) {
978
+ const existing = container.querySelector(`[data-parent-uuid="${CSS.escape(parentUuid)}"]`);
979
+ if (existing) {
980
+ const newEl = _isPromptTurnContainer(container)
981
+ ? _renderConversationNodeForEvent(msg, { expandPrompt: true })
982
+ : renderConversationEvent(msg);
983
+ if (newEl) {
984
+ _assignConversationParentUuid(newEl, parentUuid);
985
+ _replaceConversationParentEvent(existing, newEl);
986
+ seen.add(parentUuid);
987
+ }
988
+ return;
989
+ }
990
+ }
726
991
 
727
- const el = renderConversationEvent(msg);
992
+ const el = _isPromptTurnContainer(container)
993
+ ? _appendConversationEventToTurns(container, msg, { expandPrompt: true, collapseExisting: true, expandSetup: true })
994
+ : renderConversationEvent(msg);
728
995
  if (!el) return; // Skip empty events
729
- _removeConversationState(container);
730
996
  if (parentUuid) {
731
997
  _assignConversationParentUuid(el, parentUuid);
732
998
  if (seen) seen.add(parentUuid);
@@ -734,7 +1000,9 @@ function _applyStreamEvent(sessionId, container, msg) {
734
1000
  // If this is a tool-only thought-group AND the last rendered child is also
735
1001
  // a tool-only thought-group, fold this one's single step into the existing
736
1002
  // group instead of appending a new wrapper. Matches Review's grouping.
737
- if (_mergeConsecutiveToolGroups(container, el)) {
1003
+ if (_isPromptTurnContainer(container)) {
1004
+ // Already inserted into the prompt-turn body above.
1005
+ } else if (_mergeConsecutiveToolGroups(container, el)) {
738
1006
  // Already merged into the previous group — no separate row to insert.
739
1007
  } else {
740
1008
  container.appendChild(el);
@@ -765,6 +1033,26 @@ function _applyStreamEvent(sessionId, container, msg) {
765
1033
  // gap-reports/viewer-2026-04-23.md item D.
766
1034
  const CONVERSATION_PAGE_SIZE = 200;
767
1035
 
1036
+ function _conversationViewHasRenderableContent(convView) {
1037
+ if (!convView || !convView.children || convView.children.length === 0) return false;
1038
+ return Array.from(convView.children).some((child) => {
1039
+ if (!child) return false;
1040
+ if (child.classList && child.classList.contains('conversation-load-older')) return false;
1041
+ return true;
1042
+ });
1043
+ }
1044
+
1045
+ function _nextPrimeToken(sessionId) {
1046
+ const next = (_streamState._primeToken.get(sessionId) || 0) + 1;
1047
+ _streamState._primeToken.set(sessionId, next);
1048
+ return next;
1049
+ }
1050
+
1051
+ function _primeStillCurrent(sessionId, token, convView) {
1052
+ return _streamState._primeToken.get(sessionId) === token
1053
+ && _streamState._primingTarget.get(sessionId) === convView;
1054
+ }
1055
+
768
1056
  function _cleanSystemXml(t) {
769
1057
  return t
770
1058
  .replace(/<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[^>]*>[\s\S]*?<\/\1>/gi, '')
@@ -940,20 +1228,18 @@ async function _loadOlder(sessionId, convView) {
940
1228
  // Preserve scroll position: measure before + after prepending so
941
1229
  // the user's current viewport doesn't jump when older content is
942
1230
  // inserted above.
943
- const prevScrollHeight = convView.scrollHeight;
944
- const prevScrollTop = convView.scrollTop;
945
- const events = _messagesToEvents(page.messages);
946
- const frag = document.createDocumentFragment();
947
- for (const evt of events) {
948
- const el = renderConversationEvent(evt);
949
- if (el) {
950
- if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
951
- if (_mergeConsecutiveToolGroups(frag, el)) continue;
952
- frag.appendChild(el);
953
- }
954
- }
955
- // Prepend below the bar so the bar stays at the very top.
956
- convView.insertBefore(frag, bar.nextSibling);
1231
+ const prevScrollHeight = convView.scrollHeight;
1232
+ const prevScrollTop = convView.scrollTop;
1233
+ const events = _messagesToEvents(page.messages);
1234
+ const frag = document.createDocumentFragment();
1235
+ const pageHost = document.createElement('div');
1236
+ pageHost.dataset.turnMode = 'conversation';
1237
+ for (const evt of events) {
1238
+ _appendConversationEventToTurns(pageHost, evt, { expandPrompt: false, collapseExisting: false, expandSetup: false });
1239
+ }
1240
+ while (pageHost.firstChild) frag.appendChild(pageHost.firstChild);
1241
+ // Prepend below the bar so the bar stays at the very top.
1242
+ convView.insertBefore(frag, bar.nextSibling);
957
1243
  const newScrollHeight = convView.scrollHeight;
958
1244
  convView.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight);
959
1245
  // Merge new parentUuids into the dedup set so live events for primed
@@ -971,13 +1257,22 @@ async function _loadOlder(sessionId, convView) {
971
1257
  }
972
1258
 
973
1259
  async function _primeConversationView(sessionId, convView) {
974
- if (_streamState._primed.get(sessionId)) return;
1260
+ if (_streamState._primed.get(sessionId)
1261
+ && _streamState._primedTarget.get(sessionId) === convView
1262
+ && _conversationViewHasRenderableContent(convView)) {
1263
+ return;
1264
+ }
975
1265
  const inflight = _streamState._priming.get(sessionId);
976
- if (inflight) return inflight;
1266
+ if (inflight && _streamState._primingTarget.get(sessionId) === convView) return inflight;
1267
+
1268
+ const token = _nextPrimeToken(sessionId);
1269
+ _streamState._primed.delete(sessionId);
1270
+ _streamState._primedTarget.delete(sessionId);
977
1271
 
978
1272
  const promise = (async () => {
979
1273
  try {
980
1274
  const page = await _fetchConversationPage(sessionId, 0);
1275
+ if (!_primeStillCurrent(sessionId, token, convView)) return;
981
1276
  if (!page) {
982
1277
  _renderConversationState(convView, 'Transcript unavailable for this session.', 'error');
983
1278
  return;
@@ -999,11 +1294,16 @@ async function _primeConversationView(sessionId, convView) {
999
1294
  if (page.has_more) _ensureLoadOlderBar(convView, sessionId);
1000
1295
  _updateLoadOlderBar(convView, page);
1001
1296
  } catch {
1002
- _renderConversationState(convView, 'Transcript unavailable for this session.', 'error');
1297
+ if (_primeStillCurrent(sessionId, token, convView)) {
1298
+ _renderConversationState(convView, 'Transcript unavailable for this session.', 'error');
1299
+ }
1003
1300
  }
1004
1301
  finally {
1302
+ if (!_primeStillCurrent(sessionId, token, convView)) return;
1005
1303
  _streamState._primed.set(sessionId, true);
1304
+ _streamState._primedTarget.set(sessionId, convView);
1006
1305
  _streamState._priming.delete(sessionId);
1306
+ _streamState._primingTarget.delete(sessionId);
1007
1307
  // Replay any stream-events that arrived during the prime.
1008
1308
  const queue = _streamState._pendingEvents.get(sessionId);
1009
1309
  if (queue && queue.length) {
@@ -1013,14 +1313,18 @@ async function _primeConversationView(sessionId, convView) {
1013
1313
  }
1014
1314
  })();
1015
1315
  _streamState._priming.set(sessionId, promise);
1316
+ _streamState._primingTarget.set(sessionId, convView);
1016
1317
  return promise;
1017
1318
  }
1018
1319
 
1019
1320
  // Called when a session is destroyed / tab closes. Clears per-session
1020
1321
  // priming state so a re-subscribe on the same session later starts clean.
1021
1322
  function _resetPrimingState(sessionId) {
1323
+ _nextPrimeToken(sessionId);
1022
1324
  _streamState._primed.delete(sessionId);
1325
+ _streamState._primedTarget.delete(sessionId);
1023
1326
  _streamState._priming.delete(sessionId);
1327
+ _streamState._primingTarget.delete(sessionId);
1024
1328
  _streamState._pendingEvents.delete(sessionId);
1025
1329
  _streamState._parentUuidSeen.delete(sessionId);
1026
1330
  }
@@ -1054,17 +1358,38 @@ function _streamTooltipClosestSessionItem(target, root) {
1054
1358
  return item;
1055
1359
  }
1056
1360
 
1361
+ function _streamTooltipVisible() {
1362
+ const tooltip = _streamState.tooltipEl;
1363
+ return !!(tooltip && tooltip.style.display !== 'none');
1364
+ }
1365
+
1366
+ function _clearPendingStreamTooltip(invalidateRequest) {
1367
+ if (_streamState.tooltipTimer) {
1368
+ clearTimeout(_streamState.tooltipTimer);
1369
+ _streamState.tooltipTimer = null;
1370
+ }
1371
+ _streamState._tooltipPendingSessionId = null;
1372
+ if (invalidateRequest) _streamState._tooltipReqId++;
1373
+ }
1374
+
1057
1375
  function _scheduleStreamTooltip(item) {
1058
1376
  const id = item?.dataset?.sessionId;
1059
1377
  if (!id) return;
1060
- _streamState.tooltipTimer = setTimeout(() => showStreamTooltip(id, item), 500);
1378
+ if (_streamState._tooltipPendingSessionId === id) return;
1379
+ if (_streamState._tooltipActiveSessionId === id && _streamTooltipVisible()) return;
1380
+ _clearPendingStreamTooltip(true);
1381
+ _streamState._tooltipPendingSessionId = id;
1382
+ _streamState.tooltipTimer = setTimeout(() => {
1383
+ _streamState.tooltipTimer = null;
1384
+ _streamState._tooltipPendingSessionId = null;
1385
+ showStreamTooltip(id, item);
1386
+ }, 500);
1061
1387
  }
1062
1388
 
1063
1389
  function _handleStreamTooltipMouseOver(e, list) {
1064
1390
  const item = _streamTooltipClosestSessionItem(e.target, list);
1065
1391
  if (!item) return;
1066
1392
  if (e.relatedTarget && _streamTooltipContains(item, e.relatedTarget)) return;
1067
- hideStreamTooltip();
1068
1393
  _scheduleStreamTooltip(item);
1069
1394
  }
1070
1395
 
@@ -1072,6 +1397,14 @@ function _handleStreamTooltipMouseOut(e, list) {
1072
1397
  const item = _streamTooltipClosestSessionItem(e.target, list);
1073
1398
  if (!item) return;
1074
1399
  if (e.relatedTarget && _streamTooltipContains(item, e.relatedTarget)) return;
1400
+ const id = item?.dataset?.sessionId;
1401
+ if (_streamState._tooltipPendingSessionId === id) _clearPendingStreamTooltip(true);
1402
+ }
1403
+
1404
+ function _handleStreamTooltipDocumentPointerDown(e) {
1405
+ const tooltip = _streamState.tooltipEl;
1406
+ if (!tooltip || !_streamTooltipVisible()) return;
1407
+ if (_streamTooltipContains(tooltip, e.target)) return;
1075
1408
  hideStreamTooltip();
1076
1409
  }
1077
1410
 
@@ -1083,6 +1416,7 @@ function bindStreamTooltips() {
1083
1416
 
1084
1417
  list.addEventListener('mouseover', (e) => _handleStreamTooltipMouseOver(e, list));
1085
1418
  list.addEventListener('mouseout', (e) => _handleStreamTooltipMouseOut(e, list));
1419
+ document.addEventListener('pointerdown', _handleStreamTooltipDocumentPointerDown, true);
1086
1420
  }
1087
1421
 
1088
1422
  // --- Init ---
@@ -1109,6 +1443,7 @@ if (typeof module !== 'undefined' && module.exports) {
1109
1443
  module.exports = {
1110
1444
  handleStreamMessage,
1111
1445
  _primeConversationView,
1446
+ _conversationViewHasRenderableContent,
1112
1447
  _applyStreamEvent,
1113
1448
  _resetPrimingState,
1114
1449
  _conversationLookupCandidates,
@@ -1118,9 +1453,15 @@ if (typeof module !== 'undefined' && module.exports) {
1118
1453
  _mergeConsecutiveToolGroups,
1119
1454
  _streamState,
1120
1455
  bindStreamTooltips,
1456
+ showStreamTooltip,
1457
+ hideStreamTooltip,
1458
+ _refreshVisibleStreamTooltip,
1459
+ _scheduleVisibleStreamTooltipRefresh,
1121
1460
  _handleStreamTooltipMouseOver,
1122
1461
  _handleStreamTooltipMouseOut,
1462
+ _handleStreamTooltipDocumentPointerDown,
1123
1463
  _streamTooltipClosestSessionItem,
1124
1464
  _streamTooltipContains,
1465
+ _streamTooltipVisible,
1125
1466
  };
1126
1467
  }