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
@@ -20,6 +20,7 @@
20
20
  * - renderGroup(g) — render one group from groupMessages
21
21
  * - _buildCollapsible(label, color, preview, bodyEl) — Conversation tab DOM helper
22
22
  * - renderConversationEvent(evt) — Conversation row DOM element
23
+ * - refreshConversationActivityGroup(group) — recompute grouped tool UI
23
24
  *
24
25
  * Depends on global escHtml (defined in index.html). Since the renderer
25
26
  * functions are only invoked AFTER the page's body scripts run, escHtml
@@ -28,10 +29,26 @@
28
29
  (function () {
29
30
  const MR = {};
30
31
 
32
+ function _imageApiUrlForLocalPath(src) {
33
+ const value = String(src || '').trim().replace(/^<|>$/g, '');
34
+ const match = value.match(/(?:^|[\\/])images[\\/]+([^\\/?:#]+\.(?:png|jpe?g|gif|webp|svg))(?:[?#].*)?$/i);
35
+ if (!match) return null;
36
+ return '/api/images/file/' + encodeURIComponent(match[1]);
37
+ }
38
+
39
+ function _normalizeMarkdownImageSources(text) {
40
+ return String(text || '').replace(/!\[([^\]]*)\]\(([^)\s]+|<[^>]+>)\)/g, (all, alt, src) => {
41
+ const apiUrl = _imageApiUrlForLocalPath(src);
42
+ if (!apiUrl) return all;
43
+ return `![${alt}](${apiUrl})`;
44
+ });
45
+ }
46
+
31
47
  // ------------------------------------------------------------------
32
48
  // Markdown + tool-badge formatting (was index.html formatMsgText).
33
49
  // ------------------------------------------------------------------
34
50
  MR.formatMsgText = function (text) {
51
+ text = _normalizeMarkdownImageSources(text);
35
52
  // Stash tool badges with unique placeholders before markdown parsing
36
53
  const badges = [];
37
54
  text = text.replace(/\[Tool: ([^\]]+)\]/g, (_, name) => {
@@ -179,6 +196,34 @@
179
196
  return 'normal';
180
197
  };
181
198
 
199
+ const _TOOL_TOKEN_RE = /\[Tool(?: result)?:\s*([^\]]+)\]/g;
200
+
201
+ function _toolNamesFromText(text) {
202
+ const names = [];
203
+ for (const m of String(text || '').matchAll(_TOOL_TOKEN_RE)) {
204
+ const name = (m[1] || '').trim();
205
+ if (name) names.push(name);
206
+ }
207
+ return names;
208
+ }
209
+
210
+ function _summarizeNames(names, max = 4) {
211
+ const counts = new Map();
212
+ for (const raw of names || []) {
213
+ const name = String(raw || '').trim();
214
+ if (!name) continue;
215
+ counts.set(name, (counts.get(name) || 0) + 1);
216
+ }
217
+ const parts = Array.from(counts.entries()).map(([name, count]) => count > 1 ? `${name} x${count}` : name);
218
+ if (parts.length <= max) return parts.join(' · ');
219
+ return parts.slice(0, max).join(' · ') + ` · +${parts.length - max}`;
220
+ }
221
+
222
+ function _previewFromText(text, max = 90) {
223
+ const s = String(text || '').replace(/\n/g, ' ').trim();
224
+ return s.length > max ? s.slice(0, max) + '…' : s;
225
+ }
226
+
182
227
  // ------------------------------------------------------------------
183
228
  // Review-page renderers — return HTML strings. The page batches many
184
229
  // groups into one big string, sanitizes via DOMPurify, then sets
@@ -265,15 +310,17 @@
265
310
  MR.renderSelfThoughtMsg = function (m, i, stripped) {
266
311
  const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
267
312
  const textHtml = MR.formatMsgText(m.text);
268
- const preview = stripped.replace(/\n/g, ' ').slice(0, 90) + (stripped.length > 90 ? '…' : '');
269
- return '<div class="thought-group" onclick="this.classList.toggle(\'expanded\')">'
313
+ const toolSummary = _summarizeNames(_toolNamesFromText(m.text));
314
+ const preview = toolSummary || _previewFromText(stripped);
315
+ const groupClass = toolSummary ? 'thought-group tool-activity-group' : 'thought-group';
316
+ return '<div class="' + groupClass + '" onclick="this.classList.toggle(\'expanded\')">'
270
317
  + '<div class="thought-group-header">'
271
318
  + '<span class="thought-group-label"><span class="thought-chevron">▶</span>Claude</span>'
272
319
  + '<span class="thought-group-preview">' + escHtml(preview) + '</span>'
273
320
  + '<span class="thought-group-time">' + escHtml(time) + '</span>'
274
321
  + '</div>'
275
322
  + '<div class="thought-group-items">'
276
- + '<div class="review-msg assistant" data-msg-idx="' + i + '">'
323
+ + '<div class="review-msg assistant tool-activity-item" data-msg-idx="' + i + '">'
277
324
  + '<div class="msg-text">' + textHtml + '</div></div>'
278
325
  + '</div></div>';
279
326
  };
@@ -281,7 +328,9 @@
281
328
  MR.renderSelfThoughtItem = function (m, i, stripped) {
282
329
  const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
283
330
  const textHtml = MR.formatMsgText(m.text);
284
- return '<div class="review-msg assistant" data-msg-idx="' + i + '">'
331
+ const toolNames = _toolNamesFromText(m.text);
332
+ const dataTools = toolNames.length ? ' data-tool-names="' + escHtml(toolNames.join(',')) + '"' : '';
333
+ return '<div class="review-msg assistant tool-activity-item" data-msg-idx="' + i + '"' + dataTools + '>'
285
334
  + '<div class="msg-header">'
286
335
  + '<span class="msg-role">Claude</span>'
287
336
  + '<span class="msg-time">' + escHtml(time) + '</span>'
@@ -326,12 +375,15 @@
326
375
  if (groupItems.length === 1) {
327
376
  return MR.renderSelfThoughtMsg(groupItems[0].m, groupItems[0].i, groupItems[0].stripped);
328
377
  }
378
+ const toolNames = groupItems.flatMap(gi => _toolNamesFromText(gi.m.text));
379
+ const toolSummary = _summarizeNames(toolNames);
329
380
  const firstPreview = groupItems.map(gi => gi.stripped).find(s => s.length > 0) || '';
330
- const preview = firstPreview.replace(/\n/g, ' ').slice(0, 80) + (firstPreview.length > 80 ? '…' : '');
381
+ const preview = toolSummary || _previewFromText(firstPreview, 80);
331
382
  const firstTime = groupItems[0].m.timestamp ? new Date(groupItems[0].m.timestamp).toLocaleString() : '';
332
383
  const lastTime = groupItems[groupItems.length - 1].m.timestamp ? new Date(groupItems[groupItems.length - 1].m.timestamp).toLocaleString() : '';
333
384
  const timeLabel = firstTime === lastTime ? firstTime : firstTime + ' – ' + lastTime;
334
- return '<div class="thought-group" onclick="this.classList.toggle(\'expanded\')">'
385
+ const groupClass = toolSummary ? 'thought-group tool-activity-group' : 'thought-group';
386
+ return '<div class="' + groupClass + '" onclick="this.classList.toggle(\'expanded\')">'
335
387
  + '<div class="thought-group-header">'
336
388
  + '<span class="thought-group-label"><span class="thought-chevron">▶</span>Claude'
337
389
  + '<span class="thought-group-count">— ' + groupItems.length + ' steps</span></span>'
@@ -392,6 +444,41 @@
392
444
  return wrap;
393
445
  };
394
446
 
447
+ MR.refreshConversationActivityGroup = function (group) {
448
+ if (!group || !group.querySelector) return;
449
+ const items = group.querySelector('.thought-group-items');
450
+ if (!items) return;
451
+ const rows = Array.from(items.children || []);
452
+ const countEl = group.querySelector('.thought-group-count');
453
+ if (countEl) {
454
+ const stepCount = rows.length;
455
+ countEl.textContent = '— ' + stepCount + ' step' + (stepCount === 1 ? '' : 's');
456
+ }
457
+ const toolNames = [];
458
+ const previews = [];
459
+ for (const row of rows) {
460
+ const rawTools = row.dataset?.toolNames || '';
461
+ if (rawTools) {
462
+ for (const name of rawTools.split(',')) {
463
+ const clean = name.trim();
464
+ if (clean) toolNames.push(clean);
465
+ }
466
+ }
467
+ const p = row.dataset?.activityPreview || '';
468
+ if (p) previews.push(p);
469
+ }
470
+ const previewEl = group.querySelector('.thought-group-preview');
471
+ if (previewEl) {
472
+ previewEl.textContent = _summarizeNames(toolNames) || previews[0] || '';
473
+ }
474
+ const timeEl = group.querySelector('.thought-group-time');
475
+ if (timeEl) {
476
+ const f = group.dataset.firstTime || rows[0]?.dataset?.time || '';
477
+ const l = group.dataset.lastTime || rows[rows.length - 1]?.dataset?.time || '';
478
+ timeEl.textContent = (f && l && f !== l) ? f + ' – ' + l : (l || f);
479
+ }
480
+ };
481
+
395
482
  // Phase 2: Conversation rows now share the `.review-msg` CSS classes with
396
483
  // Review, run text through `MR.formatMsgText` for markdown + tool badges,
397
484
  // and get Show-more affordances + per-message timestamps. Conversation
@@ -602,8 +689,7 @@
602
689
  }
603
690
  if (isSelfThought) {
604
691
  const group = document.createElement('div');
605
- group.className = 'thought-group conv-tool-group';
606
- if (evt.data?.parentUuid) group.dataset.parentUuid = evt.data.parentUuid;
692
+ group.className = 'thought-group conv-tool-group tool-activity-group';
607
693
  group.dataset.firstTime = time;
608
694
  group.dataset.lastTime = time;
609
695
  group.addEventListener('click', () => group.classList.toggle('expanded'));
@@ -642,7 +728,11 @@
642
728
  const items = document.createElement('div');
643
729
  items.className = 'thought-group-items';
644
730
  const itemRow = document.createElement('div');
645
- itemRow.className = 'review-msg assistant';
731
+ itemRow.className = 'review-msg assistant tool-activity-item';
732
+ if (evt.data?.parentUuid) itemRow.dataset.parentUuid = evt.data.parentUuid;
733
+ if (time) itemRow.dataset.time = time;
734
+ if (toolNames.length) itemRow.dataset.toolNames = toolNames.join(',');
735
+ if (stripped) itemRow.dataset.activityPreview = _previewFromText(stripped, 90);
646
736
  const itemBody = document.createElement('div');
647
737
  itemBody.className = 'msg-text';
648
738
  if (stripped) {
@@ -665,6 +755,7 @@
665
755
  items.appendChild(itemRow);
666
756
  group.appendChild(header);
667
757
  group.appendChild(items);
758
+ MR.refreshConversationActivityGroup(group);
668
759
  return group;
669
760
  }
670
761
  // Mixed (text + maybe tools) — keep the existing per-row layout with
@@ -695,18 +786,56 @@
695
786
 
696
787
  if (evType === 'tool_result') {
697
788
  if (!text) return null;
698
- const div = document.createElement('div');
699
- // `.tool-only` lets the existing "Hide tool calls" toggle hide these
700
- // when the user opts in — matches Review's classification.
701
- div.className = 'review-msg tool-only';
702
- if (evt.data?.parentUuid) div.dataset.parentUuid = evt.data.parentUuid;
703
- const body = document.createElement('div');
704
- body.style.cssText = 'white-space:pre-wrap;color:#bbb;max-height:300px;overflow-y:auto;padding:4px 0 4px 12px;font-size:12px';
705
- body.textContent = text;
706
789
  const firstLine = (text.split('\n').find(l => l.trim()) || '').trim();
707
790
  const preview = firstLine.length > 100 ? firstLine.slice(0, 97) + '…' : firstLine;
708
- div.appendChild(MR._buildCollapsible('Tool Result', '#888', preview, body));
709
- return div;
791
+ const group = document.createElement('div');
792
+ group.className = 'thought-group conv-tool-group tool-activity-group tool-result-group';
793
+ group.dataset.firstTime = time;
794
+ group.dataset.lastTime = time;
795
+ group.addEventListener('click', () => group.classList.toggle('expanded'));
796
+
797
+ const header = document.createElement('div');
798
+ header.className = 'thought-group-header';
799
+ const label = document.createElement('span');
800
+ label.className = 'thought-group-label';
801
+ const chev = document.createElement('span');
802
+ chev.className = 'thought-chevron';
803
+ chev.textContent = '▶';
804
+ label.appendChild(chev);
805
+ const labelText = document.createElement('span');
806
+ labelText.textContent = 'Tools';
807
+ label.appendChild(labelText);
808
+ const count = document.createElement('span');
809
+ count.className = 'thought-group-count';
810
+ count.textContent = '— 1 step';
811
+ label.appendChild(count);
812
+ const previewEl = document.createElement('span');
813
+ previewEl.className = 'thought-group-preview';
814
+ previewEl.textContent = preview;
815
+ const tEl = document.createElement('span');
816
+ tEl.className = 'thought-group-time';
817
+ tEl.textContent = time;
818
+ header.appendChild(label);
819
+ header.appendChild(previewEl);
820
+ header.appendChild(tEl);
821
+
822
+ const items = document.createElement('div');
823
+ items.className = 'thought-group-items';
824
+ const itemRow = document.createElement('div');
825
+ itemRow.className = 'review-msg tool-only tool-activity-item tool-result-item';
826
+ if (evt.data?.parentUuid) itemRow.dataset.parentUuid = evt.data.parentUuid;
827
+ if (time) itemRow.dataset.time = time;
828
+ if (preview) itemRow.dataset.activityPreview = preview;
829
+ const itemBody = document.createElement('div');
830
+ itemBody.className = 'msg-text';
831
+ itemBody.style.cssText = 'white-space:pre-wrap;color:#bbb;max-height:300px;overflow-y:auto;font-size:12px';
832
+ itemBody.textContent = text;
833
+ itemRow.appendChild(itemBody);
834
+ items.appendChild(itemRow);
835
+ group.appendChild(header);
836
+ group.appendChild(items);
837
+ MR.refreshConversationActivityGroup(group);
838
+ return group;
710
839
  }
711
840
 
712
841
  if (evType === 'summary') {
@@ -18,17 +18,31 @@ let crState = {
18
18
  diffMode: 'unified', // unified | split
19
19
  selectStart: null, // Line selection start for range comments
20
20
  fileChangeCount: 0, // For badge
21
+ _view: 'projects',
22
+ _openSeq: 0,
23
+ _diffSeq: 0,
24
+ _projectSeq: 0,
21
25
  };
22
26
 
23
- function api(path, method, body) {
27
+ async function api(path, method, body) {
24
28
  const token = window._ctmState?.token || '';
25
29
  const opts = { method: method || 'GET', headers: {} };
26
30
  if (body) {
27
31
  opts.headers['Content-Type'] = 'application/json';
28
32
  opts.body = JSON.stringify(body);
29
33
  }
30
- return fetch(`/api${path}${path.includes('?') ? '&' : '?'}token=${token}`, opts)
31
- .then(r => r.json());
34
+ const res = await fetch(`/api${path}${path.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`, opts);
35
+ let data = null;
36
+ try { data = await res.json(); } catch (_) {}
37
+ if (!res.ok || (data && data.error)) {
38
+ throw new Error((data && data.error) || ('HTTP ' + res.status));
39
+ }
40
+ return data || {};
41
+ }
42
+
43
+ function normalizeReviewSeverity(severity) {
44
+ severity = String(severity || 'comment').trim().toLowerCase();
45
+ return ['comment', 'suggestion', 'issue', 'nit', 'question'].includes(severity) ? severity : 'comment';
32
46
  }
33
47
 
34
48
  // --- Pinned projects ---
@@ -46,6 +60,9 @@ CR.loadPinnedProjects = function(prefs) {
46
60
 
47
61
  // --- Show project list (default view when panel opens) ---
48
62
  CR.showProjectList = async function() {
63
+ const seq = ++crState._projectSeq;
64
+ crState._view = 'projects';
65
+ crState.reviewId = null;
49
66
  const header = document.getElementById('cr-header');
50
67
  const tree = document.getElementById('cr-file-tree');
51
68
  const area = document.getElementById('cr-diff-area');
@@ -58,6 +75,7 @@ CR.showProjectList = async function() {
58
75
 
59
76
  try {
60
77
  const data = await api('/reviews/tracked-projects');
78
+ if (seq !== crState._projectSeq || crState._view !== 'projects') return;
61
79
  const projects = data.projects || [];
62
80
 
63
81
  if (projects.length === 0) {
@@ -137,6 +155,7 @@ CR.showProjectList = async function() {
137
155
  html += '</div>';
138
156
  if (area) area.innerHTML = html;
139
157
  } catch (e) {
158
+ if (seq !== crState._projectSeq || crState._view !== 'projects') return;
140
159
  if (area) area.innerHTML = `<div class="cr-diff-empty" style="color:var(--red)">Failed to load projects: ${escHtml(e.message)}</div>`;
141
160
  }
142
161
  };
@@ -184,6 +203,8 @@ CR.openSessionForProject = function() {
184
203
 
185
204
  // --- Initialize review for a session ---
186
205
  CR.openReview = async function(sessionId, projectPath) {
206
+ const seq = ++crState._openSeq;
207
+ crState._view = 'review';
187
208
  crState.sessionId = sessionId;
188
209
  crState.projectPath = projectPath;
189
210
  crState.baseRef = '';
@@ -210,34 +231,45 @@ CR.openReview = async function(sessionId, projectPath) {
210
231
  renderHeader();
211
232
  renderLoading();
212
233
 
213
- // Load branch, commits, and rich commit log in parallel
214
- const [branchData, commitsData, logData] = await Promise.all([
215
- api(`/reviews/branch?project=${encodeURIComponent(projectPath)}`),
216
- api(`/reviews/commits?project=${encodeURIComponent(projectPath)}`),
217
- api(`/reviews/commit-log?project=${encodeURIComponent(projectPath)}&count=20`),
218
- ]);
219
- crState.branch = branchData.branch || '';
220
- crState._commitLog = logData.commits || [];
221
- renderHeader(commitsData.commits || []);
222
-
223
- // Create review record in DB
224
- const { id } = await api('/reviews', 'POST', {
225
- session_id: sessionId,
226
- project_path: projectPath,
227
- base_ref: '',
228
- });
229
- crState.reviewId = id;
234
+ try {
235
+ // Load branch, commits, and rich commit log in parallel
236
+ const [branchData, commitsData, logData] = await Promise.all([
237
+ api(`/reviews/branch?project=${encodeURIComponent(projectPath)}`),
238
+ api(`/reviews/commits?project=${encodeURIComponent(projectPath)}`),
239
+ api(`/reviews/commit-log?project=${encodeURIComponent(projectPath)}&count=20`),
240
+ ]);
241
+ if (seq !== crState._openSeq || crState._view !== 'review') return;
242
+ crState.branch = branchData.branch || '';
243
+ crState._commitLog = logData.commits || [];
244
+ renderHeader(commitsData.commits || []);
245
+
246
+ // Create review record in DB
247
+ const { id } = await api('/reviews', 'POST', {
248
+ session_id: sessionId,
249
+ project_path: projectPath,
250
+ base_ref: '',
251
+ });
252
+ if (seq !== crState._openSeq || crState._view !== 'review') return;
253
+ crState.reviewId = id;
230
254
 
231
- // Load diff
232
- await loadDiff();
255
+ // Load diff
256
+ await loadDiff();
257
+ } catch (e) {
258
+ if (seq !== crState._openSeq || crState._view !== 'review') return;
259
+ const area = document.getElementById('cr-diff-area');
260
+ if (area) area.innerHTML = `<div class="cr-diff-empty" style="color:var(--red)">Failed to open review: ${escHtml(e.message)}</div>`;
261
+ if (typeof window.toast === 'function') window.toast('Failed to open review: ' + e.message, { type: 'error' });
262
+ }
233
263
  };
234
264
 
235
265
  async function loadDiff() {
266
+ const seq = ++crState._diffSeq;
236
267
  renderLoading();
237
268
  try {
238
269
  const data = await api(
239
270
  `/reviews/diff?project=${encodeURIComponent(crState.projectPath)}&base=${encodeURIComponent(crState.baseRef)}`
240
271
  );
272
+ if (seq !== crState._diffSeq || crState._view !== 'review') return;
241
273
  crState.files = data.files || [];
242
274
 
243
275
  // Update file count on review record
@@ -253,6 +285,7 @@ async function loadDiff() {
253
285
  renderDiff();
254
286
  }
255
287
  } catch (e) {
288
+ if (seq !== crState._diffSeq || crState._view !== 'review') return;
256
289
  const area = document.getElementById('cr-diff-area');
257
290
  if (area) area.innerHTML = `<div class="cr-diff-empty" style="color:var(--red)">Failed to load diff: ${escHtml(e.message)}</div>`;
258
291
  }
@@ -282,8 +315,8 @@ function renderHeader(commits) {
282
315
  // logMap keyed by both full SHA and short SHA for flexible lookup
283
316
  const logMap = {};
284
317
  for (const lc of (crState._commitLog || [])) { logMap[lc.sha] = lc; logMap[lc.shortSha] = lc; }
285
- let baseOptions = '<option value="">Working tree (unstaged)</option>';
286
- baseOptions += '<option value="--staged">Staged changes</option>';
318
+ let baseOptions = `<option value=""${crState.baseRef === '' ? ' selected' : ''}>Working tree (unstaged)</option>`;
319
+ baseOptions += `<option value="--staged"${crState.baseRef === '--staged' ? ' selected' : ''}>Staged changes</option>`;
287
320
  if (commits) {
288
321
  for (const c of commits) {
289
322
  const sel = crState.baseRef === c.sha ? ' selected' : '';
@@ -345,10 +378,13 @@ function renderLoading() {
345
378
  function renderNoChanges() {
346
379
  const area = document.getElementById('cr-diff-area');
347
380
  const tree = document.getElementById('cr-file-tree');
381
+ const message = crState.baseRef === '--staged'
382
+ ? 'No staged changes'
383
+ : (crState.baseRef ? `Commit ${crState.baseRef.slice(0,7)} has no file changes` : 'Working tree is clean against HEAD');
348
384
  if (area) area.innerHTML = `<div class="cr-no-changes">
349
385
  <div class="cr-no-changes-icon">&#x2714;</div>
350
386
  <div class="cr-no-changes-text">No changes detected</div>
351
- <div style="font-size:11px;color:var(--fg-dim)">${crState.baseRef ? `Commit ${crState.baseRef.slice(0,7)} has no file changes` : 'Working tree is clean against HEAD'}</div>
387
+ <div style="font-size:11px;color:var(--fg-dim)">${escHtml(message)}</div>
352
388
  </div>`;
353
389
  if (tree) tree.innerHTML = '';
354
390
  renderFooter();
@@ -595,16 +631,16 @@ function _setupScrollSpy(area) {
595
631
  function renderCommentThread(comments, filePath, lineNum, side) {
596
632
  let html = `<tr class="cr-comment-row"><td colspan="4"><div class="cr-comment-thread">`;
597
633
  for (const c of comments) {
598
- const severityCls = c.severity || 'comment';
634
+ const severityCls = normalizeReviewSeverity(c.severity);
599
635
  const lineRange = c.line_end && c.line_end !== c.line_start
600
636
  ? `Lines ${c.line_start}-${c.line_end}`
601
637
  : `Line ${c.line_start}`;
602
638
  html += `<div class="cr-comment-item" data-comment-id="${c.id}">
603
639
  <div class="cr-comment-header">
604
640
  <span class="cr-comment-author">${c.ai_generated ? 'AI' : 'You'}</span>
605
- <span class="cr-comment-severity ${severityCls}">${severityCls}</span>
641
+ <span class="cr-comment-severity ${severityCls}">${escHtml(severityCls)}</span>
606
642
  ${c.ai_generated ? '<span class="cr-comment-ai">AI suggestion</span>' : ''}
607
- <span style="font-size:10px;color:var(--fg-dim);margin-left:auto">${lineRange}</span>
643
+ <span style="font-size:10px;color:var(--fg-dim);margin-left:auto">${escHtml(lineRange)}</span>
608
644
  </div>
609
645
  <div class="cr-comment-body">${escHtml(c.body)}</div>
610
646
  <div class="cr-comment-actions">
@@ -623,9 +659,9 @@ function renderFooter() {
623
659
  const footer = document.getElementById('cr-footer');
624
660
  if (!footer) return;
625
661
  const openComments = crState.comments.filter(c => c.status === 'open');
626
- const issues = openComments.filter(c => c.severity === 'issue').length;
627
- const suggestions = openComments.filter(c => c.severity === 'suggestion').length;
628
- const nits = openComments.filter(c => c.severity === 'nit').length;
662
+ const issues = openComments.filter(c => normalizeReviewSeverity(c.severity) === 'issue').length;
663
+ const suggestions = openComments.filter(c => normalizeReviewSeverity(c.severity) === 'suggestion').length;
664
+ const nits = openComments.filter(c => normalizeReviewSeverity(c.severity) === 'nit').length;
629
665
  const other = openComments.length - issues - suggestions - nits;
630
666
  const fileCount = crState.files.length;
631
667
 
@@ -779,24 +815,29 @@ CR.saveComment = async function(filePath, lineStart, lineEnd, side) {
779
815
  if (!input || !input.value.trim()) return;
780
816
 
781
817
  const body = input.value.trim();
782
- const severity = severitySelect?.value || 'comment';
818
+ const severity = normalizeReviewSeverity(severitySelect?.value);
783
819
 
784
820
  if (!crState.reviewId) return;
785
821
 
786
- const { id } = await api(`/reviews/${crState.reviewId}/comments`, 'POST', {
787
- file_path: filePath,
788
- line_start: lineStart,
789
- line_end: lineEnd,
790
- side: side,
791
- body: body,
792
- severity: severity,
793
- });
822
+ try {
823
+ const { id } = await api(`/reviews/${crState.reviewId}/comments`, 'POST', {
824
+ file_path: filePath,
825
+ line_start: lineStart,
826
+ line_end: lineEnd,
827
+ side: side === 'old' ? 'old' : 'new',
828
+ body: body,
829
+ severity: severity,
830
+ });
794
831
 
795
- crState.comments.push({
796
- id, review_id: crState.reviewId, file_path: filePath,
797
- line_start: lineStart, line_end: lineEnd, side,
798
- body, severity, status: 'open', ai_generated: 0,
799
- });
832
+ crState.comments.push({
833
+ id, review_id: crState.reviewId, file_path: filePath,
834
+ line_start: lineStart, line_end: lineEnd, side: side === 'old' ? 'old' : 'new',
835
+ body, severity, status: 'open', ai_generated: 0,
836
+ });
837
+ } catch (e) {
838
+ if (typeof window.toast === 'function') window.toast('Failed to save comment: ' + e.message, { type: 'error' });
839
+ return;
840
+ }
800
841
 
801
842
  // Remove form and re-render
802
843
  renderDiff();
@@ -804,26 +845,38 @@ CR.saveComment = async function(filePath, lineStart, lineEnd, side) {
804
845
  };
805
846
 
806
847
  CR.resolveComment = async function(commentId) {
807
- await api(`/review-comments/${commentId}`, 'PUT', { status: 'resolved' });
808
- const c = crState.comments.find(c => c.id === commentId);
809
- if (c) c.status = 'resolved';
810
- renderDiff();
811
- renderFileTree();
848
+ try {
849
+ await api(`/review-comments/${commentId}`, 'PUT', { status: 'resolved' });
850
+ const c = crState.comments.find(c => c.id === commentId);
851
+ if (c) c.status = 'resolved';
852
+ renderDiff();
853
+ renderFileTree();
854
+ } catch (e) {
855
+ if (typeof window.toast === 'function') window.toast('Failed to resolve comment: ' + e.message, { type: 'error' });
856
+ }
812
857
  };
813
858
 
814
859
  CR.reopenComment = async function(commentId) {
815
- await api(`/review-comments/${commentId}`, 'PUT', { status: 'open' });
816
- const c = crState.comments.find(c => c.id === commentId);
817
- if (c) c.status = 'open';
818
- renderDiff();
819
- renderFileTree();
860
+ try {
861
+ await api(`/review-comments/${commentId}`, 'PUT', { status: 'open' });
862
+ const c = crState.comments.find(c => c.id === commentId);
863
+ if (c) c.status = 'open';
864
+ renderDiff();
865
+ renderFileTree();
866
+ } catch (e) {
867
+ if (typeof window.toast === 'function') window.toast('Failed to reopen comment: ' + e.message, { type: 'error' });
868
+ }
820
869
  };
821
870
 
822
871
  CR.deleteComment = async function(commentId) {
823
- await api(`/review-comments/${commentId}`, 'DELETE');
824
- crState.comments = crState.comments.filter(c => c.id !== commentId);
825
- renderDiff();
826
- renderFileTree();
872
+ try {
873
+ await api(`/review-comments/${commentId}`, 'DELETE');
874
+ crState.comments = crState.comments.filter(c => c.id !== commentId);
875
+ renderDiff();
876
+ renderFileTree();
877
+ } catch (e) {
878
+ if (typeof window.toast === 'function') window.toast('Failed to delete comment: ' + e.message, { type: 'error' });
879
+ }
827
880
  };
828
881
 
829
882
  // --- Submit review: compose prompt and send to session ---
@@ -877,9 +930,14 @@ async function _composeReviewPrompt() {
877
930
  if (typeof window.toast === 'function') window.toast('No open comments to submit', { type: 'warning' });
878
931
  return null;
879
932
  }
880
- await api(`/reviews/${crState.reviewId}`, 'PUT', { status: 'submitted' });
881
- const { prompt } = await api(`/reviews/${crState.reviewId}/compose`);
882
- return prompt;
933
+ try {
934
+ const { prompt } = await api(`/reviews/${crState.reviewId}/compose`);
935
+ await api(`/reviews/${crState.reviewId}`, 'PUT', { status: 'submitted' });
936
+ return prompt;
937
+ } catch (e) {
938
+ if (typeof window.toast === 'function') window.toast('Failed to compose review: ' + e.message, { type: 'error' });
939
+ return null;
940
+ }
883
941
  }
884
942
 
885
943
  // Main "Send" button — auto-detects best session (existing project session or new)