@yemi33/squad 0.1.13 → 0.1.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.
package/dashboard.html CHANGED
@@ -47,6 +47,48 @@
47
47
  .status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
48
48
  .status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
49
49
  .agent-action { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
50
+ .modal-qa { border-top: 1px solid var(--border); padding: 10px 20px; }
51
+ .modal-qa-thread { max-height: 200px; overflow-y: auto; margin-bottom: 8px; }
52
+ .modal-qa-q { font-size: 12px; color: var(--blue); margin-bottom: 4px; font-weight: 600; }
53
+ .modal-qa-q .selection-ref { font-weight: 400; color: var(--muted); font-style: italic; display: block; font-size: 10px; margin-top: 2px; }
54
+ .modal-qa-a { font-size: 12px; color: var(--text); margin-bottom: 12px; padding: 8px 10px; background: var(--surface2); border-radius: 6px; border-left: 2px solid var(--blue); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
55
+ .modal-qa-loading { font-size: 11px; color: var(--muted); padding: 8px 10px; display: flex; align-items: center; gap: 8px; }
56
+ .modal-qa-loading .dot-pulse { display: inline-flex; gap: 3px; }
57
+ .modal-qa-loading .dot-pulse span { width: 5px; height: 5px; background: var(--blue); border-radius: 50%; animation: dotPulse 1.2s infinite; }
58
+ .modal-qa-loading .dot-pulse span:nth-child(2) { animation-delay: 0.2s; }
59
+ .modal-qa-loading .dot-pulse span:nth-child(3) { animation-delay: 0.4s; }
60
+ @keyframes dotPulse { 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
61
+ .modal-qa-selection-pill { display: flex; align-items: center; gap: 6px; padding: 4px 8px; margin-bottom: 6px; background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.25); border-radius: 4px; font-size: 11px; }
62
+ .modal-qa-selection-pill .pill-label { color: var(--blue); font-weight: 600; white-space: nowrap; }
63
+ .modal-qa-selection-pill .pill-text { color: var(--muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
64
+ .modal-qa-selection-pill .pill-clear { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 14px; padding: 0 2px; line-height: 1; }
65
+ .modal-qa-selection-pill .pill-clear:hover { color: var(--red); }
66
+ .modal-qa-input-wrap { display: flex; gap: 6px; }
67
+ .modal-qa-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 6px 10px; font-size: 12px; color: var(--text); font-family: inherit; }
68
+ .modal-qa-input:focus { border-color: var(--blue); outline: none; }
69
+ .modal-qa-btn { background: var(--blue); color: #fff; border: none; border-radius: 4px; padding: 6px 14px; font-size: 12px; cursor: pointer; }
70
+ .modal-qa-btn:hover { opacity: 0.9; }
71
+ .modal-qa-btn:disabled { opacity: 0.4; cursor: not-allowed; }
72
+ .ask-selection-btn { display: none; position: fixed; z-index: 500; background: var(--blue); color: #fff; font-size: 11px; padding: 5px 12px; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.3); }
73
+ .ask-selection-btn:hover { opacity: 0.9; }
74
+ .plan-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 12px; margin-bottom: 8px; }
75
+ .plan-card.awaiting { border-left: 3px solid var(--yellow, #d29922); }
76
+ .plan-card.approved { border-left: 3px solid var(--green); }
77
+ .plan-card.rejected { border-left: 3px solid var(--red); opacity: 0.6; }
78
+ .plan-card.revision-requested { border-left: 3px solid var(--purple, #a855f7); }
79
+ .plan-card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
80
+ .plan-card-title { font-size: 13px; font-weight: 600; color: var(--text); }
81
+ .plan-card-meta { font-size: 10px; color: var(--muted); margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; }
82
+ .plan-card-actions { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
83
+ .plan-btn { font-size: 11px; padding: 4px 10px; border-radius: 4px; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--text); transition: all 0.15s; }
84
+ .plan-btn:hover { border-color: var(--text); }
85
+ .plan-btn.approve { color: var(--green); border-color: var(--green); }
86
+ .plan-btn.approve:hover { background: rgba(63,185,80,0.1); }
87
+ .plan-btn.revise { color: var(--yellow, #d29922); border-color: var(--yellow, #d29922); }
88
+ .plan-btn.revise:hover { background: rgba(210,153,34,0.1); }
89
+ .plan-btn.reject { color: var(--red); border-color: var(--red); }
90
+ .plan-btn.reject:hover { background: rgba(248,81,73,0.1); }
91
+ .plan-feedback-input { width: 100%; margin-top: 6px; padding: 6px 8px; font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-family: inherit; resize: vertical; min-height: 50px; }
50
92
  .token-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }
51
93
  .token-tile { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }
52
94
  .token-tile-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
@@ -202,7 +244,19 @@
202
244
  transition: border-color 0.2s, box-shadow 0.2s; padding: 0;
203
245
  }
204
246
  .cmd-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(88,166,255,0.12); }
247
+ .cmd-highlight-layer {
248
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
249
+ padding: 14px 16px; font-size: 14px; font-family: inherit; line-height: 1.5;
250
+ white-space: pre-wrap; word-wrap: break-word; overflow: hidden;
251
+ pointer-events: none; color: transparent; border-radius: 10px;
252
+ }
253
+ .cmd-highlight-layer .hl-cmd { color: transparent; background: rgba(88,166,255,0.18); border-radius: 3px; }
254
+ .cmd-highlight-layer .hl-mention { color: transparent; background: rgba(63,185,80,0.18); border-radius: 3px; }
255
+ .cmd-highlight-layer .hl-priority { color: transparent; background: rgba(210,153,34,0.18); border-radius: 3px; }
256
+ .cmd-highlight-layer .hl-project { color: transparent; background: rgba(188,140,255,0.18); border-radius: 3px; }
257
+ .cmd-highlight-layer .hl-flag { color: transparent; background: rgba(248,81,73,0.18); border-radius: 3px; }
205
258
  .cmd-input-wrap textarea {
259
+ position: relative; z-index: 1;
206
260
  flex: 1; background: transparent; border: none; color: var(--text);
207
261
  padding: 14px 16px; font-size: 14px; font-family: inherit; resize: none;
208
262
  outline: none; line-height: 1.5; min-height: 48px; max-height: 200px;
@@ -321,11 +375,15 @@
321
375
  .dispatch-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }
322
376
  .dispatch-type { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 8px; text-transform: uppercase; white-space: nowrap; }
323
377
  .dispatch-type.implement { background: rgba(88,166,255,0.15); color: var(--blue); }
378
+ .dispatch-type.implement\:large { background: rgba(88,166,255,0.25); color: var(--blue); }
324
379
  .dispatch-type.review { background: rgba(188,140,255,0.15); color: var(--purple); }
325
380
  .dispatch-type.fix { background: rgba(210,153,34,0.15); color: var(--yellow); }
326
381
  .dispatch-type.analyze { background: rgba(63,185,80,0.15); color: var(--green); }
327
382
  .dispatch-type.explore { background: rgba(139,148,158,0.15); color: var(--muted); }
328
383
  .dispatch-type.test { background: rgba(227,179,65,0.15); color: var(--orange); }
384
+ .dispatch-type.plan { background: rgba(168,85,247,0.15); color: #a855f7; }
385
+ .dispatch-type.plan-to-prd { background: rgba(168,85,247,0.1); color: #a855f7; }
386
+ .dispatch-type.ask { background: rgba(63,185,80,0.15); color: var(--green); }
329
387
  .dispatch-type.manual { background: rgba(139,148,158,0.15); color: var(--muted); }
330
388
  .dispatch-agent { font-weight: 600; color: var(--text); }
331
389
  .dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -441,8 +499,9 @@
441
499
  <section class="cmd-center">
442
500
  <h2>Command Center</h2>
443
501
  <div class="cmd-input-wrap" id="cmd-input-wrap">
502
+ <div class="cmd-highlight-layer" id="cmd-highlight" aria-hidden="true"></div>
444
503
  <textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas", "explain the dispatch flow", or "/note always use feature flags"'
445
- oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)"></textarea>
504
+ oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)" onscroll="syncHighlightScroll()"></textarea>
446
505
  <button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
447
506
  </div>
448
507
  <div class="cmd-mention-popup" id="cmd-mention-popup"></div>
@@ -450,10 +509,12 @@
450
509
  <div class="cmd-hints">
451
510
  <span><code>@agent</code> assign</span>
452
511
  <span><code>@everyone</code> fan-out</span>
512
+ <span><code>#project</code> target</span>
453
513
  <span><code>!high</code> / <code>!low</code> priority</span>
454
- <span><code>/note</code> team note</span>
514
+ <span><code>/plan</code> feature plan</span>
455
515
  <span><code>/prd</code> PRD item</span>
456
- <span><code>#project</code> target project</span>
516
+ <span><code>/note</code> team note</span>
517
+ <span>or just type a task</span>
457
518
  <button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
458
519
  </div>
459
520
  <div class="cmd-toast" id="cmd-toast"></div>
@@ -482,7 +543,12 @@
482
543
  </section>
483
544
 
484
545
  <section>
485
- <h2>Notes Inbox <span class="count" id="inbox-count">0</span></h2>
546
+ <h2>Plans <span class="count" id="plans-count">0</span></h2>
547
+ <div id="plans-list"><p class="empty">No plans yet. Use /plan in the command center to create one.</p></div>
548
+ </section>
549
+
550
+ <section>
551
+ <h2>Notes Inbox <span class="count" id="inbox-count">0</span> <span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">auto-consolidates at 3 notes</span></h2>
486
552
  <div class="inbox-list" id="inbox-list">Loading...</div>
487
553
  </section>
488
554
 
@@ -502,6 +568,11 @@
502
568
  <div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
503
569
  </section>
504
570
 
571
+ <section>
572
+ <h2>MCP Servers <span class="count" id="mcp-count">0</span></h2>
573
+ <div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
574
+ </section>
575
+
505
576
  <section>
506
577
  <h2>Dispatch Queue</h2>
507
578
  <div class="dispatch-stats" id="dispatch-stats"></div>
@@ -555,9 +626,24 @@
555
626
  </div>
556
627
  </div>
557
628
  <div class="modal-body" id="modal-body"></div>
629
+ <div class="modal-qa" id="modal-qa">
630
+ <div class="modal-qa-thread" id="modal-qa-thread"></div>
631
+ <div class="modal-qa-selection-pill" id="modal-qa-pill" style="display:none">
632
+ <span class="pill-label">Selection:</span>
633
+ <span class="pill-text" id="modal-qa-pill-text"></span>
634
+ <button class="pill-clear" onclick="clearQaSelection()" title="Clear selection">&times;</button>
635
+ </div>
636
+ <div class="modal-qa-input-wrap">
637
+ <input type="text" class="modal-qa-input" id="modal-qa-input" placeholder="Ask about this document (or select text first)..." onkeydown="if(event.key==='Enter')modalAskSubmit()">
638
+ <button class="modal-qa-btn" id="modal-qa-btn" onclick="modalAskSubmit()">Ask</button>
639
+ </div>
640
+ </div>
558
641
  </div>
559
642
  </div>
560
643
 
644
+ <!-- Floating "Ask about selection" button -->
645
+ <div class="ask-selection-btn" id="ask-selection-btn" onclick="modalAskAboutSelection()">Ask about this</div>
646
+
561
647
  <script>
562
648
  let inboxData = [];
563
649
  let agentData = [];
@@ -668,7 +754,26 @@ function renderDetailContent(detail, tab) {
668
754
  } else if (tab === 'charter') {
669
755
  el.innerHTML = '<div class="section">' + escHtml(detail.charter || 'No charter found.') + '</div>';
670
756
  } else if (tab === 'history') {
671
- el.innerHTML = '<div class="section">' + escHtml(detail.history || 'No history yet.') + '</div>';
757
+ let html = '';
758
+ // Recent dispatch results
759
+ if (detail.recentDispatches && detail.recentDispatches.length > 0) {
760
+ html += '<h4>Recent Dispatches</h4><table class="pr-table" style="margin-bottom:16px"><thead><tr><th>Task</th><th>Type</th><th>Result</th><th>Completed</th></tr></thead><tbody>';
761
+ detail.recentDispatches.forEach(d => {
762
+ const isError = d.result === 'error';
763
+ const color = isError ? 'var(--red)' : 'var(--green)';
764
+ const reason = d.reason ? ' title="' + escHtml(d.reason) + '"' : '';
765
+ html += '<tr>' +
766
+ '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(d.task) + '">' + escHtml(d.task.slice(0, 80)) + '</td>' +
767
+ '<td><span class="dispatch-type ' + d.type + '">' + escHtml(d.type) + '</span></td>' +
768
+ '<td style="color:' + color + '"' + reason + '>' + escHtml(d.result) + (isError && d.reason ? ' <span style="font-size:10px;color:var(--muted)">(' + escHtml(d.reason.slice(0, 50)) + ')</span>' : '') + '</td>' +
769
+ '<td style="font-size:10px;color:var(--muted)">' + (d.completed_at ? new Date(d.completed_at).toLocaleString() : '') + '</td>' +
770
+ '</tr>';
771
+ });
772
+ html += '</tbody></table>';
773
+ }
774
+ // Raw history.md
775
+ html += '<h4>Task History</h4><div class="section">' + escHtml(detail.history || 'No history yet.') + '</div>';
776
+ el.innerHTML = html;
672
777
  } else if (tab === 'output') {
673
778
  el.innerHTML = '<div class="section">' + escHtml(detail.outputLog || 'No output log. The coordinator will save agent output here when tasks complete.') + '</div>';
674
779
  }
@@ -777,23 +882,45 @@ function renderInbox(inbox) {
777
882
  <span>${escHtml(item.name)}</span><span>${item.age}</span>
778
883
  </div>
779
884
  <div class="inbox-preview" onclick="openModal(${i})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
780
- <div style="display:flex;gap:6px;margin-top:6px">
781
- <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();persistInboxItem('${escHtml(item.name)}')">Persist as Note</button>
885
+ <div style="display:flex;gap:6px;margin-top:6px;align-items:center">
886
+ <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();promoteToKB('${escHtml(item.name)}')">Add to Knowledge Base</button>
782
887
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();openInboxInExplorer('${escHtml(item.name)}')">Open in Explorer</button>
783
888
  </div>
784
889
  </div>
785
890
  `).join('');
786
891
  }
787
892
 
788
- async function persistInboxItem(name) {
893
+ function promoteToKB(name) {
894
+ const categories = [
895
+ { id: 'architecture', label: 'Architecture' },
896
+ { id: 'conventions', label: 'Conventions' },
897
+ { id: 'project-notes', label: 'Project Notes' },
898
+ { id: 'build-reports', label: 'Build Reports' },
899
+ { id: 'reviews', label: 'Reviews' },
900
+ ];
901
+ const picker = '<div style="padding:16px 20px">' +
902
+ '<p style="font-size:13px;color:var(--text);margin-bottom:12px">Choose a category for <strong>' + escHtml(name) + '</strong>:</p>' +
903
+ '<div style="display:flex;flex-direction:column;gap:8px">' +
904
+ categories.map(c =>
905
+ '<button class="pr-pager-btn" style="font-size:12px;padding:8px 16px;text-align:left" onclick="doPromoteToKB(\'' + escHtml(name) + '\',\'' + c.id + '\')">' + c.label + '</button>'
906
+ ).join('') +
907
+ '</div></div>';
908
+ document.getElementById('modal-title').textContent = 'Add to Knowledge Base';
909
+ document.getElementById('modal-body').innerHTML = picker;
910
+ document.getElementById('modal').classList.add('open');
911
+ }
912
+
913
+ async function doPromoteToKB(name, category) {
789
914
  try {
790
- const res = await fetch('/api/inbox/persist', {
915
+ const res = await fetch('/api/inbox/promote-kb', {
791
916
  method: 'POST', headers: { 'Content-Type': 'application/json' },
792
- body: JSON.stringify({ name })
917
+ body: JSON.stringify({ name, category })
793
918
  });
794
919
  const data = await res.json();
795
920
  if (res.ok) {
921
+ closeModal();
796
922
  refresh();
923
+ refreshKnowledgeBase();
797
924
  } else {
798
925
  alert('Failed: ' + (data.error || 'unknown'));
799
926
  }
@@ -929,10 +1056,21 @@ function openModal(i) {
929
1056
  const item = inboxData[i];
930
1057
  if (!item) return;
931
1058
  document.getElementById('modal-title').textContent = item.name;
932
- document.getElementById('modal-body').textContent = item.content;
1059
+ document.getElementById('modal-body').innerHTML =
1060
+ '<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
1061
+ '<pre style="white-space:pre-wrap;word-wrap:break-word;margin:0;font-family:Consolas,monospace;font-size:12px;line-height:1.7;color:var(--muted)">' + escHtml(item.content) + '</pre>';
933
1062
  document.getElementById('modal').classList.add('open');
934
1063
  }
935
- function closeModal() { document.getElementById('modal').classList.remove('open'); }
1064
+ function closeModal() {
1065
+ document.getElementById('modal').classList.remove('open');
1066
+ // Clear Q&A state
1067
+ _modalDocContext = { title: '', content: '', selection: '' };
1068
+ document.getElementById('modal-qa-thread').innerHTML = '';
1069
+ document.getElementById('modal-qa-input').value = '';
1070
+ document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
1071
+ document.getElementById('modal-qa-pill').style.display = 'none';
1072
+ document.getElementById('ask-selection-btn').style.display = 'none';
1073
+ }
936
1074
 
937
1075
  document.addEventListener('keydown', e => {
938
1076
  if (e.key === 'Escape') { closeDetail(); closeModal(); }
@@ -1173,9 +1311,10 @@ async function refresh() {
1173
1311
  renderMetrics(data.metrics || {});
1174
1312
  renderWorkItems(data.workItems || []);
1175
1313
  renderSkills(data.skills || []);
1176
- // Refresh KB less frequently (every 3rd cycle = ~12s)
1314
+ renderMcpServers(data.mcpServers || []);
1315
+ // Refresh KB and plans less frequently (every 3rd cycle = ~12s)
1177
1316
  if (!window._kbRefreshCount) window._kbRefreshCount = 0;
1178
- if (window._kbRefreshCount++ % 3 === 0) refreshKnowledgeBase();
1317
+ if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
1179
1318
  } catch(e) { console.error('refresh error', e); }
1180
1319
  }
1181
1320
 
@@ -1201,6 +1340,25 @@ function renderProjects(projects) {
1201
1340
 
1202
1341
  }
1203
1342
 
1343
+ // -- MCP Servers --
1344
+ function renderMcpServers(servers) {
1345
+ const el = document.getElementById('mcp-list');
1346
+ const countEl = document.getElementById('mcp-count');
1347
+ countEl.textContent = servers.length;
1348
+ if (!servers.length) {
1349
+ el.innerHTML = '<p class="empty">No MCP servers found. Add them to <code>~/.claude.json</code> and they\'ll appear here automatically.</p>';
1350
+ return;
1351
+ }
1352
+ el.innerHTML = '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">' +
1353
+ servers.map(s =>
1354
+ '<div style="font-size:11px;padding:5px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text)" title="' + escHtml(s.args || s.command) + '">' +
1355
+ escHtml(s.name) +
1356
+ '</div>'
1357
+ ).join('') +
1358
+ '</div>' +
1359
+ '<p style="font-size:10px;color:var(--muted);margin:0">Synced from <code style="color:var(--blue)">~/.claude.json</code> — add MCP servers there to make them available to all agents.</p>';
1360
+ }
1361
+
1204
1362
  // -- Squad Skills --
1205
1363
  function renderSkills(skills) {
1206
1364
  const el = document.getElementById('skills-list');
@@ -1526,7 +1684,7 @@ function detectWorkItemType(text) {
1526
1684
  const t = text.toLowerCase();
1527
1685
  const patterns = [
1528
1686
  { type: 'ask', words: ['explain', 'why does', 'why is', 'what does', 'how do i', 'how do you', 'what\'s the', 'tell me', 'can you explain', 'walk me through'] },
1529
- { type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase'] },
1687
+ { type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase', 'make a note of', 'find out'] },
1530
1688
  { type: 'fix', words: ['fix', 'bug', 'broken', 'crash', 'error', 'issue', 'patch', 'repair', 'resolve', 'regression', 'failing', 'doesn\'t work', 'not working'] },
1531
1689
  { type: 'review', words: ['review', 'code review', 'check pr', 'look at pr', 'audit code', 'inspect'] },
1532
1690
  { type: 'test', words: ['test', 'write tests', 'add tests', 'unit test', 'e2e test', 'coverage', 'testing', 'build', 'run locally', 'localhost', 'start the', 'spin up', 'verify', 'check if it works'] },
@@ -1557,18 +1715,18 @@ function cmdParseInput(raw) {
1557
1715
  if (/^\/decide\b/i.test(text) || /^\/note\b/i.test(text) || rememberPattern.test(text)) {
1558
1716
  result.intent = 'note';
1559
1717
  text = text.replace(/^\/decide\s*/i, '').replace(/^\/note\s*/i, '').replace(rememberPattern, '').trim();
1560
- } else if (/^\/plan\b/i.test(text)) {
1718
+ } else if (/^\/plan\b/i.test(text) || /^(make a plan|plan out|plan for|plan how|create a plan|design a plan|come up with a plan|draft a plan|write a plan)\b/i.test(text)) {
1561
1719
  result.intent = 'plan';
1562
- text = text.replace(/^\/plan\s*/i, '');
1720
+ text = text.replace(/^\/plan\s*/i, '').replace(/^(make a plan for|plan out how|plan for how|plan how|create a plan for|design a plan for|come up with a plan for|draft a plan for|write a plan for|make a plan|plan out|create a plan|design a plan|come up with a plan|draft a plan|write a plan)\s*/i, '').trim();
1563
1721
  // Extract branch strategy flag
1564
1722
  if (/--parallel\b/i.test(text)) {
1565
1723
  result.branchStrategy = 'parallel';
1566
1724
  text = text.replace(/--parallel\b/i, '').trim();
1567
- } else if (/--shared\b/i.test(text)) {
1725
+ } else if (/--stack\b/i.test(text)) {
1568
1726
  result.branchStrategy = 'shared-branch';
1569
- text = text.replace(/--shared\b/i, '').trim();
1727
+ text = text.replace(/--stack\b/i, '').trim();
1570
1728
  } else {
1571
- result.branchStrategy = 'shared-branch'; // default
1729
+ result.branchStrategy = 'parallel'; // default — items without depends_on get independent branches
1572
1730
  }
1573
1731
  } else if (/^\/prd\b/i.test(text)) {
1574
1732
  result.intent = 'prd';
@@ -1631,19 +1789,19 @@ function cmdRenderMeta() {
1631
1789
  let chips = [];
1632
1790
 
1633
1791
  // Intent chip
1634
- const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item', 'plan': 'Plan → PRD → Dispatch' };
1635
- chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
1792
+ if (parsed.intent === 'plan') {
1793
+ const strategy = parsed.branchStrategy || 'parallel';
1794
+ const stratLabel = strategy === 'parallel' ? 'parallel branches' : 'stacked';
1795
+ chips.push('<span class="cmd-chip" style="background:var(--purple,#a855f7);color:#fff">Plan → PRD → Dispatch (' + stratLabel + ')</span>');
1796
+ } else {
1797
+ const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item' };
1798
+ chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
1799
+ }
1636
1800
 
1637
1801
  // Type chip (only for work items)
1638
1802
  if (parsed.intent === 'work-item') {
1639
1803
  chips.push('<span class="cmd-chip">' + parsed.type + '</span>');
1640
1804
  }
1641
- // Pipeline chip for /plan
1642
- if (parsed.intent === 'plan') {
1643
- const strategy = parsed.branchStrategy || 'shared-branch';
1644
- const stratLabel = strategy === 'parallel' ? 'parallel branches' : 'shared branch';
1645
- chips.push('<span class="cmd-chip" style="background:var(--purple,#a855f7);color:#fff">plan → prd → agents (' + stratLabel + ')</span>');
1646
- }
1647
1805
 
1648
1806
  // Priority chip
1649
1807
  chips.push('<span class="cmd-chip priority-' + parsed.priority + '">' + parsed.priority + ' priority</span>');
@@ -1751,8 +1909,30 @@ function cmdAutoResize() {
1751
1909
  ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
1752
1910
  }
1753
1911
 
1912
+ function cmdUpdateHighlight() {
1913
+ const text = document.getElementById('cmd-input').value;
1914
+ const hl = document.getElementById('cmd-highlight');
1915
+ if (!text) { hl.innerHTML = ''; return; }
1916
+ // Escape HTML then wrap tokens with highlight spans
1917
+ let html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1918
+ html = html.replace(/(\/(?:plan|prd|note|decide)\b)/gi, '<span class="hl-cmd">$1</span>');
1919
+ html = html.replace(/(@\w+)/g, '<span class="hl-mention">$1</span>');
1920
+ html = html.replace(/(![a-z]+\b)/gi, '<span class="hl-priority">$1</span>');
1921
+ html = html.replace(/(#\S+)/g, '<span class="hl-project">$1</span>');
1922
+ html = html.replace(/(--(?:stack|parallel)\b)/gi, '<span class="hl-flag">$1</span>');
1923
+ hl.innerHTML = html + '\n'; // trailing newline prevents layout shift
1924
+ }
1925
+
1926
+ function syncHighlightScroll() {
1927
+ const ta = document.getElementById('cmd-input');
1928
+ const hl = document.getElementById('cmd-highlight');
1929
+ hl.scrollTop = ta.scrollTop;
1930
+ hl.scrollLeft = ta.scrollLeft;
1931
+ }
1932
+
1754
1933
  function cmdInputChanged() {
1755
1934
  cmdAutoResize();
1935
+ cmdUpdateHighlight();
1756
1936
  cmdRenderMeta();
1757
1937
 
1758
1938
  // Check for @ mention or # project trigger
@@ -1933,7 +2113,7 @@ async function cmdSubmitPlan(parsed) {
1933
2113
  title: parsed.title,
1934
2114
  description: parsed.description || '',
1935
2115
  priority: parsed.priority,
1936
- branch_strategy: parsed.branchStrategy || 'shared-branch',
2116
+ branch_strategy: parsed.branchStrategy || 'parallel',
1937
2117
  };
1938
2118
  if (parsed.project) body.project = parsed.project;
1939
2119
  if (parsed.agents.length === 1) body.agent = parsed.agents[0];
@@ -1968,6 +2148,313 @@ async function cmdSubmitPrd(parsed) {
1968
2148
  const projLabel = (parsed.projects || []).length > 0 ? ' (' + parsed.projects.join(', ') + ')' : '';
1969
2149
  showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added' + projLabel, true);
1970
2150
  }
2151
+ // ─── Modal Q&A (Ask about document) ──────────────────────────────────────────
2152
+
2153
+ let _modalDocContext = { title: '', content: '', selection: '' };
2154
+
2155
+ // Track text selection anywhere in content areas for the floating "Ask about this" button
2156
+ document.addEventListener('mouseup', function(e) {
2157
+ // Small delay to let the selection finalize
2158
+ setTimeout(() => {
2159
+ const btn = document.getElementById('ask-selection-btn');
2160
+ if (!btn) return;
2161
+ const sel = window.getSelection();
2162
+ const text = sel?.toString()?.trim();
2163
+
2164
+ if (!text || text.length < 3) {
2165
+ btn.style.display = 'none';
2166
+ return;
2167
+ }
2168
+
2169
+ // Check if selection is in any content area: modal body, detail panel, notes, kb
2170
+ const anchor = sel?.anchorNode;
2171
+ if (!anchor) { btn.style.display = 'none'; return; }
2172
+
2173
+ const modalBody = document.getElementById('modal-body');
2174
+ const detailContent = document.getElementById('detail-content');
2175
+ const isInModal = modalBody?.contains(anchor);
2176
+ const isInDetail = detailContent?.contains(anchor);
2177
+
2178
+ if (isInModal || isInDetail) {
2179
+ _modalDocContext.selection = text;
2180
+ // If we don't have document context yet (e.g. detail panel), capture it
2181
+ if (!_modalDocContext.content && isInDetail) {
2182
+ _modalDocContext.content = detailContent.textContent || '';
2183
+ _modalDocContext.title = document.getElementById('detail-agent-name')?.textContent || 'Agent Detail';
2184
+ }
2185
+ btn.style.display = 'block';
2186
+ // Position near the cursor but within viewport
2187
+ const x = Math.min(e.clientX, window.innerWidth - 140);
2188
+ const y = Math.max(e.clientY - 35, 10);
2189
+ btn.style.left = x + 'px';
2190
+ btn.style.top = y + 'px';
2191
+ btn.style.position = 'fixed';
2192
+ } else {
2193
+ btn.style.display = 'none';
2194
+ }
2195
+ }, 10);
2196
+ });
2197
+
2198
+ function modalAskAboutSelection() {
2199
+ document.getElementById('ask-selection-btn').style.display = 'none';
2200
+
2201
+ // If the modal isn't open but we have a selection (from detail panel), open modal for Q&A
2202
+ const modal = document.getElementById('modal');
2203
+ if (!modal.classList.contains('open')) {
2204
+ document.getElementById('modal-title').textContent = 'Q&A: ' + (_modalDocContext.title || 'Document');
2205
+ document.getElementById('modal-body').textContent = _modalDocContext.content.slice(0, 3000) + (_modalDocContext.content.length > 3000 ? '\n\n...(truncated for display)' : '');
2206
+ modal.classList.add('open');
2207
+ }
2208
+
2209
+ // Show the selection pill
2210
+ const pill = document.getElementById('modal-qa-pill');
2211
+ const pillText = document.getElementById('modal-qa-pill-text');
2212
+ const sel = _modalDocContext.selection || '';
2213
+ if (sel) {
2214
+ pillText.textContent = sel.slice(0, 80) + (sel.length > 80 ? '...' : '');
2215
+ pill.style.display = 'flex';
2216
+ }
2217
+
2218
+ const input = document.getElementById('modal-qa-input');
2219
+ input.value = '';
2220
+ input.placeholder = 'What do you want to know about this?';
2221
+ input.focus();
2222
+ }
2223
+
2224
+ function clearQaSelection() {
2225
+ _modalDocContext.selection = '';
2226
+ document.getElementById('modal-qa-pill').style.display = 'none';
2227
+ document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
2228
+ }
2229
+
2230
+ async function modalAskSubmit() {
2231
+ const input = document.getElementById('modal-qa-input');
2232
+ const question = input.value.trim();
2233
+ if (!question) return;
2234
+
2235
+ // Capture content from modal body if not already set
2236
+ if (!_modalDocContext.content) {
2237
+ const body = document.getElementById('modal-body');
2238
+ if (body) {
2239
+ _modalDocContext.content = body.textContent || body.innerText || '';
2240
+ _modalDocContext.title = document.getElementById('modal-title')?.textContent || '';
2241
+ }
2242
+ }
2243
+ if (!_modalDocContext.content) {
2244
+ showToast('cmd-toast', 'No document content to ask about', false);
2245
+ return;
2246
+ }
2247
+
2248
+ const thread = document.getElementById('modal-qa-thread');
2249
+ const btn = document.getElementById('modal-qa-btn');
2250
+
2251
+ // Show question
2252
+ let qHtml = '<div class="modal-qa-q">' + escHtml(question);
2253
+ if (_modalDocContext.selection) {
2254
+ qHtml += '<span class="selection-ref">Re: "' + escHtml(_modalDocContext.selection.slice(0, 100)) + ((_modalDocContext.selection.length > 100) ? '...' : '') + '"</span>';
2255
+ }
2256
+ qHtml += '</div>';
2257
+ thread.innerHTML += qHtml;
2258
+
2259
+ // Show loading
2260
+ const loadingId = 'qa-loading-' + Date.now();
2261
+ thread.innerHTML += '<div class="modal-qa-loading" id="' + loadingId + '"><div class="dot-pulse"><span></span><span></span><span></span></div> Thinking...</div>';
2262
+ thread.scrollTop = thread.scrollHeight;
2263
+
2264
+ input.value = '';
2265
+ input.placeholder = 'Ask another question...';
2266
+ btn.disabled = true;
2267
+
2268
+ try {
2269
+ const res = await fetch('/api/ask-about', {
2270
+ method: 'POST',
2271
+ headers: { 'Content-Type': 'application/json' },
2272
+ body: JSON.stringify({
2273
+ question,
2274
+ document: _modalDocContext.content,
2275
+ title: _modalDocContext.title,
2276
+ selection: _modalDocContext.selection || '',
2277
+ }),
2278
+ });
2279
+ const data = await res.json();
2280
+ const loadingEl = document.getElementById(loadingId);
2281
+ if (loadingEl) loadingEl.remove();
2282
+
2283
+ if (data.ok && data.answer) {
2284
+ thread.innerHTML += '<div class="modal-qa-a">' + escHtml(data.answer) + '</div>';
2285
+ } else {
2286
+ thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'No answer') + '</div>';
2287
+ }
2288
+ } catch (e) {
2289
+ const loadingEl = document.getElementById(loadingId);
2290
+ if (loadingEl) loadingEl.remove();
2291
+ thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(e.message) + '</div>';
2292
+ }
2293
+
2294
+ _modalDocContext.selection = ''; // Clear selection after asking
2295
+ document.getElementById('modal-qa-pill').style.display = 'none';
2296
+ document.getElementById('modal-qa-input').placeholder = 'Ask another question...';
2297
+ btn.disabled = false;
2298
+ thread.scrollTop = thread.scrollHeight;
2299
+ input.focus();
2300
+ }
2301
+
2302
+ // Override closeModal to clear Q&A state
2303
+ const _origCloseModal = typeof closeModal === 'function' ? closeModal : null;
2304
+
2305
+ // ─── Plans (Approval Gate) ────────────────────────────────────────────────────
2306
+
2307
+ async function refreshPlans() {
2308
+ try {
2309
+ const plans = await fetch('/api/plans').then(r => r.json());
2310
+ renderPlans(plans);
2311
+ } catch {}
2312
+ }
2313
+
2314
+ function renderPlans(plans) {
2315
+ const el = document.getElementById('plans-list');
2316
+ const countEl = document.getElementById('plans-count');
2317
+ countEl.textContent = plans.length;
2318
+
2319
+ if (plans.length === 0) {
2320
+ el.innerHTML = '<p class="empty">No plans yet. Use /plan in the command center to create one.</p>';
2321
+ return;
2322
+ }
2323
+
2324
+ const statusLabels = { 'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected', 'revision-requested': 'Revision Requested', 'completed': 'Completed', 'active': 'Active' };
2325
+ const statusClass = (s) => s === 'awaiting-approval' ? 'awaiting' : s || '';
2326
+
2327
+ el.innerHTML = plans.map(p => {
2328
+ const status = p.status || 'active';
2329
+ const label = statusLabels[status] || status;
2330
+ const needsAction = status === 'awaiting-approval';
2331
+ const isRevision = status === 'revision-requested';
2332
+
2333
+ let actions = '';
2334
+ if (needsAction) {
2335
+ actions = '<div class="plan-card-actions">' +
2336
+ '<button class="plan-btn approve" onclick="planApprove(\'' + escHtml(p.file) + '\')">Approve</button>' +
2337
+ '<button class="plan-btn" style="color:var(--blue);border-color:var(--blue)" onclick="planDiscuss(\'' + escHtml(p.file) + '\')">Discuss &amp; Revise</button>' +
2338
+ '<button class="plan-btn reject" onclick="planReject(\'' + escHtml(p.file) + '\')">Reject</button>' +
2339
+ '<button class="plan-btn" onclick="planView(\'' + escHtml(p.file) + '\')">View Full Plan</button>' +
2340
+ '</div>' +
2341
+ '<div id="revise-input-' + escHtml(p.file).replace(/\./g, '-') + '" style="display:none">' +
2342
+ '<textarea class="plan-feedback-input" placeholder="What should be changed? Be specific..." id="revise-feedback-' + escHtml(p.file).replace(/\./g, '-') + '"></textarea>' +
2343
+ '<div class="plan-card-actions" style="margin-top:4px">' +
2344
+ '<button class="plan-btn revise" onclick="planSubmitRevise(\'' + escHtml(p.file) + '\')">Submit Revision Request</button>' +
2345
+ '<button class="plan-btn" onclick="planHideRevise(\'' + escHtml(p.file) + '\')">Cancel</button>' +
2346
+ '</div>' +
2347
+ '</div>';
2348
+ } else if (isRevision) {
2349
+ actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
2350
+ } else if (status === 'approved' || status === 'active') {
2351
+ actions = '<div class="plan-card-actions"><button class="plan-btn" onclick="planView(\'' + escHtml(p.file) + '\')">View</button></div>';
2352
+ }
2353
+
2354
+ return '<div class="plan-card ' + statusClass(status) + '">' +
2355
+ '<div class="plan-card-header">' +
2356
+ '<div><div class="plan-card-title">' + escHtml(p.summary || p.file) + '</div>' +
2357
+ '<div class="plan-card-meta">' +
2358
+ '<span style="font-weight:600;color:' + (needsAction ? 'var(--yellow,#d29922)' : status === 'approved' ? 'var(--green)' : 'var(--muted)') + '">' + label + '</span>' +
2359
+ '<span>' + escHtml(p.project) + '</span>' +
2360
+ '<span>' + p.itemCount + ' items</span>' +
2361
+ '<span>' + escHtml(p.branchStrategy) + '</span>' +
2362
+ (p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
2363
+ (p.generatedAt ? '<span>' + p.generatedAt + '</span>' : '') +
2364
+ '</div>' +
2365
+ '</div>' +
2366
+ '</div>' +
2367
+ actions +
2368
+ '</div>';
2369
+ }).join('');
2370
+ }
2371
+
2372
+ async function planApprove(file) {
2373
+ try {
2374
+ await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
2375
+ showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
2376
+ refreshPlans();
2377
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2378
+ }
2379
+
2380
+ async function planReject(file) {
2381
+ if (!confirm('Reject this plan? It will not be executed.')) return;
2382
+ const reason = prompt('Reason for rejection (optional):') || '';
2383
+ try {
2384
+ await fetch('/api/plans/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, reason }) });
2385
+ showToast('cmd-toast', 'Plan rejected', true);
2386
+ refreshPlans();
2387
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2388
+ }
2389
+
2390
+ function planShowRevise(file) {
2391
+ const id = 'revise-input-' + file.replace(/\./g, '-');
2392
+ document.getElementById(id).style.display = 'block';
2393
+ }
2394
+
2395
+ function planHideRevise(file) {
2396
+ const id = 'revise-input-' + file.replace(/\./g, '-');
2397
+ document.getElementById(id).style.display = 'none';
2398
+ }
2399
+
2400
+ async function planSubmitRevise(file) {
2401
+ const id = 'revise-feedback-' + file.replace(/\./g, '-');
2402
+ const feedback = document.getElementById(id).value.trim();
2403
+ if (!feedback) { showToast('cmd-toast', 'Please enter feedback', false); return; }
2404
+ try {
2405
+ const res = await fetch('/api/plans/revise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, feedback }) });
2406
+ const data = await res.json();
2407
+ showToast('cmd-toast', 'Revision requested — agent will update the plan (' + data.workItemId + ')', true);
2408
+ planHideRevise(file);
2409
+ refreshPlans();
2410
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
2411
+ }
2412
+
2413
+ async function planDiscuss(file) {
2414
+ try {
2415
+ const res = await fetch('/api/plans/discuss', {
2416
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2417
+ body: JSON.stringify({ file })
2418
+ });
2419
+ const data = await res.json();
2420
+ if (!res.ok) throw new Error(data.error);
2421
+
2422
+ // Show the launch command in a modal
2423
+ const content = `To discuss and revise this plan interactively, run this command in a terminal:\n\n` +
2424
+ `━━━ Bash / Git Bash ━━━\n${data.command}\n\n` +
2425
+ `━━━ PowerShell ━━━\n${data.psCommand}\n\n` +
2426
+ `━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
2427
+ `This launches an interactive Claude session with the plan pre-loaded.\n` +
2428
+ `Chat naturally to review and refine. When you're satisfied, say "approve" and the session will write the approved plan back to disk.\n\n` +
2429
+ `The engine will pick it up on the next tick and start dispatching work.`;
2430
+
2431
+ document.getElementById('modal-title').textContent = 'Discuss Plan: ' + file;
2432
+ document.getElementById('modal-body').textContent = content;
2433
+ document.getElementById('modal').classList.add('open');
2434
+ } catch (e) {
2435
+ showToast('cmd-toast', 'Error: ' + e.message, false);
2436
+ }
2437
+ }
2438
+
2439
+ async function planView(file) {
2440
+ try {
2441
+ const plan = await fetch('/api/plans/' + encodeURIComponent(file)).then(r => r.json());
2442
+ const items = (plan.missing_features || []).map((f, i) =>
2443
+ (i + 1) + '. [' + f.id + '] ' + f.name + ' (' + f.estimated_complexity + ', ' + f.priority + ')' +
2444
+ (f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : '') +
2445
+ '\n ' + (f.description || '').slice(0, 150)
2446
+ ).join('\n\n');
2447
+ const text = 'Project: ' + plan.project + '\nStrategy: ' + (plan.branch_strategy || 'parallel') +
2448
+ '\nBranch: ' + (plan.feature_branch || 'per-item') +
2449
+ '\nStatus: ' + (plan.status || 'active') +
2450
+ '\n\n--- Items ---\n\n' + items +
2451
+ (plan.open_questions?.length ? '\n\n--- Open Questions ---\n\n' + plan.open_questions.join('\n') : '');
2452
+ document.getElementById('modal-title').textContent = plan.plan_summary || file;
2453
+ document.getElementById('modal-body').textContent = text;
2454
+ document.getElementById('modal').classList.add('open');
2455
+ } catch (e) { console.error(e); }
2456
+ }
2457
+
1971
2458
  // ─── Knowledge Base ──────────────────────────────────────────────────────────
1972
2459
  let _kbData = {};
1973
2460
  let _kbActiveTab = 'all';