@hanzlaa/rcode 3.4.32 → 3.5.0
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/AGENTS.md +6 -6
- package/CONTRIBUTING.md +2 -0
- package/LICENSE +21 -0
- package/README.md +66 -403
- package/cli/agent.js +3 -2
- package/cli/doctor.js +87 -1
- package/cli/install.js +122 -31
- package/cli/lib/schemas.cjs +318 -0
- package/cli/postinstall.js +19 -3
- package/dist/rcode.js +318 -24
- package/package.json +8 -4
- package/rihal/agents/rihal-cross-platform-auditor.md +15 -0
- package/rihal/agents/rihal-dep-auditor.md +15 -0
- package/rihal/agents/rihal-docs-auditor.md +3 -145
- package/rihal/agents/rihal-i18n-auditor.md +16 -0
- package/rihal/agents/rihal-nyquist-auditor.md +4 -156
- package/rihal/agents/rihal-observability-auditor.md +16 -0
- package/rihal/agents/rihal-phase-researcher.md +1 -1
- package/rihal/agents/rihal-planner.md +1 -1
- package/rihal/bin/rihal-hooks.cjs +394 -4
- package/rihal/bin/rihal-tools.cjs +891 -24
- package/rihal/commands/create-prd.md +18 -0
- package/rihal/commands/execute-milestone.md +18 -0
- package/rihal/commands/plan-milestone.md +18 -0
- package/rihal/commands/scaffold-milestone.md +18 -0
- package/rihal/commands/scaffold-skill.md +18 -0
- package/rihal/references/REFERENCES_INDEX.md +49 -7
- package/rihal/references/agent-contracts.md +10 -0
- package/rihal/references/design-tokens.md +98 -0
- package/rihal/references/docs-auditor-playbook.md +148 -0
- package/rihal/references/git-preflight.md +117 -0
- package/rihal/references/iterative-retrieval.md +85 -0
- package/rihal/references/nyquist-auditor-playbook.md +157 -0
- package/rihal/references/workstream-flag.md +2 -2
- package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +7 -3
- package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
- package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
- package/rihal/skills/agents/majlis-council/SKILL.md +1 -1
- package/rihal/team.yaml +32 -0
- package/rihal/templates/settings-hooks.json +39 -0
- package/rihal/workflows/check-todos.md +4 -0
- package/rihal/workflows/code-review-fix.md +4 -3
- package/rihal/workflows/code-review.md +1 -1
- package/rihal/workflows/debug.md +1 -1
- package/rihal/workflows/dev-story.md +4 -0
- package/rihal/workflows/diff.md +2 -2
- package/rihal/workflows/do.md +16 -8
- package/rihal/workflows/docs-update.md +2 -2
- package/rihal/workflows/enable-hooks.md +6 -1
- package/rihal/workflows/execute-milestone.md +139 -0
- package/rihal/workflows/execute-regression-gates.md +1 -1
- package/rihal/workflows/execute-sprint.md +54 -2
- package/rihal/workflows/execute-verify-phase-goal.md +31 -4
- package/rihal/workflows/execute-waves.md +33 -5
- package/rihal/workflows/execute.md +40 -6
- package/rihal/workflows/help.md +1 -1
- package/rihal/workflows/import.md +1 -1
- package/rihal/workflows/lens-audit.md +39 -23
- package/rihal/workflows/list-workspaces.md +1 -1
- package/rihal/workflows/map-codebase.md +4 -4
- package/rihal/workflows/new-milestone.md +18 -1
- package/rihal/workflows/new-project-research.md +53 -1
- package/rihal/workflows/new-workspace.md +1 -1
- package/rihal/workflows/plan-milestone.md +105 -0
- package/rihal/workflows/plan-research-validation.md +1 -1
- package/rihal/workflows/plan-spawn-planner.md +1 -1
- package/rihal/workflows/plan.md +31 -3
- package/rihal/workflows/plant-seed.md +6 -0
- package/rihal/workflows/quick.md +11 -5
- package/rihal/workflows/research-phase.md +24 -0
- package/rihal/workflows/scaffold-milestone.md +60 -0
- package/rihal/workflows/scaffold-skill.md +137 -0
- package/rihal/workflows/scan.md +1 -1
- package/rihal/workflows/session-report.md +43 -3
- package/rihal/workflows/verify-work.md +3 -3
- package/server/dashboard.js +53 -6
- package/server/lib/api.js +7 -0
- package/server/lib/html/client.js +725 -13
- package/server/lib/html/css.js +2046 -466
- package/server/lib/html/shell.js +227 -134
- package/server/lib/scanner.js +33 -0
- package/server/orchestrator.js +438 -0
|
@@ -14,6 +14,10 @@ function renderClientJs(state) {
|
|
|
14
14
|
last_session: state.raw?.last_session || null,
|
|
15
15
|
chains: state.raw?.chains || [],
|
|
16
16
|
workstreams: state.raw?.workstreams || [],
|
|
17
|
+
// #12 — passthrough scanner-computed fields (absent values stay undefined,
|
|
18
|
+
// both UI blocks below guard with `if (S.pendingHandoff)` / `if (S.memoryBank…)`).
|
|
19
|
+
pendingHandoff: state.pendingHandoff || null,
|
|
20
|
+
memoryBank: state.memoryBank || null,
|
|
17
21
|
});
|
|
18
22
|
|
|
19
23
|
return `<script>
|
|
@@ -29,9 +33,9 @@ function chip(s) {
|
|
|
29
33
|
: s === 'blocked' ? 'blocked'
|
|
30
34
|
: s === 'planned' ? 'planned'
|
|
31
35
|
: s === 'todo' ? 'todo' : 'other';
|
|
32
|
-
return '<span class="status-chip ' + c + '">● ' + s + '</span>';
|
|
36
|
+
return '<span class="status-chip ' + c + '">● ' + esc(s) + '</span>';
|
|
33
37
|
}
|
|
34
|
-
function tag(t) { return '<span class="tag">' + t + '</span>'; }
|
|
38
|
+
function tag(t) { return '<span class="tag">' + esc(t) + '</span>'; }
|
|
35
39
|
function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
36
40
|
function pct(d, t) { return t > 0 ? Math.round(d/t*100) + '%' : '—'; }
|
|
37
41
|
function pctNum(d, t) { return t > 0 ? Math.round(d/t*100) : 0; }
|
|
@@ -316,11 +320,40 @@ function renderOverview() {
|
|
|
316
320
|
chainsHtml += '</div></section>';
|
|
317
321
|
}
|
|
318
322
|
|
|
323
|
+
// #12 — pending handoff banner (shown only when .rihal/HANDOFF.json present).
|
|
324
|
+
// Read-only — the dashboard never resumes; user runs /rihal-resume-work.
|
|
325
|
+
let handoffHtml = '';
|
|
326
|
+
if (S.pendingHandoff) {
|
|
327
|
+
const ho = S.pendingHandoff;
|
|
328
|
+
const when = ho.ts ? humanDate(ho.ts) : '';
|
|
329
|
+
const summary = ho.summary ? ' — ' + esc(ho.summary).slice(0, 120) : '';
|
|
330
|
+
const where = ho.sprint ? ' [sprint ' + esc(ho.sprint) + ']' :
|
|
331
|
+
ho.phase ? ' [phase ' + esc(ho.phase) + ']' : '';
|
|
332
|
+
handoffHtml = '<section style="border-left:4px solid var(--accent-orange,#f59e0b);padding-left:var(--space-3);">' +
|
|
333
|
+
'<h2>⚠ Pending Handoff</h2><div class="body">' +
|
|
334
|
+
'<div>' + (when ? esc(when) : '') + where + summary + '</div>' +
|
|
335
|
+
(ho.resume_hint ? '<div style="margin-top:var(--space-2);color:var(--text-secondary);font-size:var(--text-sm);">' + esc(ho.resume_hint) + '</div>' : '') +
|
|
336
|
+
'<div style="margin-top:var(--space-3);font-size:var(--text-sm);"><code>/rihal-resume-work</code></div>' +
|
|
337
|
+
'</div></section>';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// #12 — memory bank summary (shown only when .rihal/context/active.md present).
|
|
341
|
+
let memoryHtml = '';
|
|
342
|
+
if (S.memoryBank && S.memoryBank.active) {
|
|
343
|
+
const m = S.memoryBank.active;
|
|
344
|
+
memoryHtml = '<section><h2>🧠 Memory Bank</h2><div class="body">' +
|
|
345
|
+
'<div class="attr-grid">' +
|
|
346
|
+
attr('active.md', m.lines + ' lines · ' + Math.round(m.bytes / 1024 * 10) / 10 + ' KB') +
|
|
347
|
+
attr('Updated', humanDate(m.updated)) +
|
|
348
|
+
'</div></div></section>';
|
|
349
|
+
}
|
|
350
|
+
|
|
319
351
|
const el = document.getElementById('view-overview-dynamic');
|
|
320
352
|
// Overview hints
|
|
321
353
|
var oHints = [cmdHint('/rihal-next', 'What should I do next?'), cmdHint('/rihal-status', 'Quick project status'), cmdHint('/rihal-council', 'Ask the team a question')];
|
|
322
354
|
if (curSprint) { oHints = sprintHints(curSprint).concat(oHints); }
|
|
323
|
-
if (
|
|
355
|
+
if (S.pendingHandoff) { oHints.unshift(cmdHint('/rihal-resume-work', 'Resume from the pending handoff')); }
|
|
356
|
+
if (el) el.innerHTML = handoffHtml + sprintProgressHtml + memoryHtml + velocityHtml + councilHtml + chainsHtml + lastSessionHtml + cmdAccordion(oHints);
|
|
324
357
|
}
|
|
325
358
|
|
|
326
359
|
function renderRoadmap() {
|
|
@@ -482,8 +515,11 @@ function renderPhases(subId) {
|
|
|
482
515
|
// #282: completed_at date
|
|
483
516
|
(p.completed_at ? attr('Completed', humanDate(p.completed_at)) : '') + '</div></div>' +
|
|
484
517
|
'<div style="margin-bottom:var(--space-4);">' + progressBar(done, stories.length) + '</div>' +
|
|
485
|
-
|
|
486
|
-
|
|
518
|
+
'<div class="term-action-bar">' +
|
|
519
|
+
'<button class="term-run-btn" onclick="runAndOpenTerm(\\'phase-' + esc(p.id) + '\\',\\'/rihal-execute\\',\\'Phase ' + esc(p.id) + '\\')">▶ Run Phase</button>' +
|
|
520
|
+
'<button class="term-run-btn outline" onclick="openTermPanel(\\'phase-' + esc(p.id) + '\\',\\'Phase ' + esc(p.id) + '\\')">📟 Terminal</button>' +
|
|
521
|
+
'<button class="back-btn" onclick="viewPlanFile(\\\'' + esc(p.id) + '\\\')">📄 View plan file →</button>' +
|
|
522
|
+
'</div>' +
|
|
487
523
|
velocityHtml +
|
|
488
524
|
'<div class="view-title" style="margin-top:var(--space-6)">Sprints</div>' +
|
|
489
525
|
'<div class="phase-list">' + (sps.length ? sps.map(s => sprintCard(Object.assign({},s,{phaseId:p.id,phaseName:p.name}))).join('') :
|
|
@@ -531,8 +567,12 @@ function renderSprints(subId) {
|
|
|
531
567
|
(s.started_at ? attr('Started', humanDate(s.started_at)) : '') +
|
|
532
568
|
(s.completed_at ? attr('Completed', humanDate(s.completed_at)) : '') + '</div></div>' +
|
|
533
569
|
// #289: progress bar
|
|
534
|
-
'<div style="margin-bottom:var(--space-
|
|
535
|
-
'<div class="
|
|
570
|
+
'<div style="margin-bottom:var(--space-4);">' + progressBar(done, stories.length) + '</div>' +
|
|
571
|
+
'<div class="term-action-bar">' +
|
|
572
|
+
'<button class="term-run-btn" onclick="runAndOpenTerm(\\'sprint-' + esc(s.id) + '\\',\\'/rihal-execute-sprint ' + esc(s.id) + '\\',\\'Sprint ' + esc(s.id) + '\\')">▶ Run Sprint</button>' +
|
|
573
|
+
'<button class="term-run-btn outline" onclick="openTermPanel(\\'sprint-' + esc(s.id) + '\\',\\'Sprint ' + esc(s.id) + '\\')">📟 Terminal</button>' +
|
|
574
|
+
'</div>' +
|
|
575
|
+
'<div class="view-title" style="margin-top:var(--space-4)">Tasks</div>' +
|
|
536
576
|
'<div class="phase-list">' + (stories.length ? stories.map(taskCard).join('') :
|
|
537
577
|
'<div class="empty">No tasks in this sprint yet.<div class="empty-action">Run /rihal-create-story to add tasks</div></div>') + '</div>' +
|
|
538
578
|
acHtml + cmdAccordion(sprintHints(s));
|
|
@@ -622,6 +662,512 @@ function sortTasks() {
|
|
|
622
662
|
if (el) el.innerHTML = sort === 'default' ? renderTasksGrouped(tasks) : tasks.map(taskCard).join('');
|
|
623
663
|
}
|
|
624
664
|
|
|
665
|
+
// ── Kanban + Orchestrator ────────────────────────────────────────────────────
|
|
666
|
+
// Run/Stop talk to orchestrator on :7718.
|
|
667
|
+
// Terminals render in the side panel (#orch-panel), not inline in cards.
|
|
668
|
+
var ORCH = 'http://localhost:7718';
|
|
669
|
+
var _orchStreams = {}; // Map<storyId, EventSource>
|
|
670
|
+
var _panelActive = null; // currently active storyId in panel
|
|
671
|
+
var _sessions = {}; // Map<storyId, { title, termEl, fileOpBuf[] }>
|
|
672
|
+
|
|
673
|
+
function kanbanCol(status) {
|
|
674
|
+
if (status === 'done' || status === 'completed') return 'done';
|
|
675
|
+
if (status === 'in_progress' || status === 'active' || status === 'running') return 'in_progress';
|
|
676
|
+
if (status === 'blocked') return 'blocked';
|
|
677
|
+
return 'todo';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── Kanban render ────────────────────────────────────────────────
|
|
681
|
+
function renderKanban() {
|
|
682
|
+
const el = document.getElementById('view-kanban');
|
|
683
|
+
if (!el) return;
|
|
684
|
+
const tasks = allTasks();
|
|
685
|
+
const cols = [
|
|
686
|
+
{ id: 'todo', label: 'Todo', cssClass: 'col-todo' },
|
|
687
|
+
{ id: 'in_progress', label: 'In Progress', cssClass: 'col-prog' },
|
|
688
|
+
{ id: 'blocked', label: 'Blocked', cssClass: 'col-blocked' },
|
|
689
|
+
{ id: 'done', label: 'Done', cssClass: 'col-done' },
|
|
690
|
+
];
|
|
691
|
+
const buckets = { todo: [], in_progress: [], blocked: [], done: [] };
|
|
692
|
+
for (const t of tasks) buckets[kanbanCol(t.status)].push(t);
|
|
693
|
+
|
|
694
|
+
// Topbar
|
|
695
|
+
let h = '<div class="kanban-topbar">' +
|
|
696
|
+
'<div class="kanban-topbar-title">' +
|
|
697
|
+
'<span class="orch-status-dot" id="orch-dot"></span>' +
|
|
698
|
+
'Kanban' +
|
|
699
|
+
'</div>' +
|
|
700
|
+
'<div class="kanban-topbar-actions">' +
|
|
701
|
+
'<button class="kanban-refresh-btn" onclick="refreshOrchestratorStatus()">⟳ Sync</button>' +
|
|
702
|
+
'<button class="kanban-refresh-btn" onclick="openOrchPanel(null)" style="margin-left:4px;">⊞ Sessions</button>' +
|
|
703
|
+
'</div>' +
|
|
704
|
+
'</div>';
|
|
705
|
+
|
|
706
|
+
if (!tasks.length) {
|
|
707
|
+
el.innerHTML = h + '<div class="empty" style="margin:24px;">' +
|
|
708
|
+
'No stories yet.<div class="empty-action">/rihal-plan to generate tasks</div></div>';
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
h += '<div class="kanban-board">';
|
|
713
|
+
for (const col of cols) {
|
|
714
|
+
const items = buckets[col.id];
|
|
715
|
+
h += '<div class="kanban-col ' + col.cssClass + '" data-col="' + col.id + '">' +
|
|
716
|
+
'<div class="kanban-col-head">' +
|
|
717
|
+
'<span class="col-label"><span class="col-status-dot"></span>' + esc(col.label) + '</span>' +
|
|
718
|
+
'<span class="kanban-count">' + items.length + '</span>' +
|
|
719
|
+
'</div>' +
|
|
720
|
+
'<div class="kanban-col-body">';
|
|
721
|
+
for (const t of items) {
|
|
722
|
+
const c = kanbanCol(t.status);
|
|
723
|
+
const sid = esc(t.id || '');
|
|
724
|
+
const canRun = c === 'todo' || c === 'blocked';
|
|
725
|
+
const isRunning = c === 'in_progress';
|
|
726
|
+
const pts = t.points ? t.points + 'p' : null;
|
|
727
|
+
const phase = t.phaseId ? 'P' + t.phaseId : null;
|
|
728
|
+
const sprintMeta = [pts, phase].filter(Boolean).join(' · ');
|
|
729
|
+
const actionBtn = sid
|
|
730
|
+
? (canRun
|
|
731
|
+
? '<button class="kanban-run-btn" data-action="run">▶ Run</button>'
|
|
732
|
+
: isRunning
|
|
733
|
+
? '<button class="kanban-stop-btn" data-action="stop">■ Stop</button>' +
|
|
734
|
+
'<button class="kanban-view-btn" data-action="view">↗ View</button>'
|
|
735
|
+
: '<button class="kanban-view-btn" data-action="view">↗ Logs</button>')
|
|
736
|
+
: '';
|
|
737
|
+
h += '<div class="kanban-card s-' + c + (isRunning ? ' running' : '') +
|
|
738
|
+
'" data-story-id="' + sid + '" draggable="true">' +
|
|
739
|
+
'<div class="kanban-card-header">' +
|
|
740
|
+
'<div class="kanban-card-title">' + esc(t.title || t.id || 'Untitled') + '</div>' +
|
|
741
|
+
(sid ? '<div class="kanban-card-id">' + sid.slice(0, 8) + '</div>' : '') +
|
|
742
|
+
'</div>' +
|
|
743
|
+
(sprintMeta ? '<div class="kanban-card-meta">' +
|
|
744
|
+
'<span class="kanban-card-sprint">' + esc(sprintMeta) + '</span>' +
|
|
745
|
+
'<span class="kanban-card-status">' + esc(col.label) + '</span>' +
|
|
746
|
+
'</div>' : '') +
|
|
747
|
+
(isRunning ? '<div class="card-run-indicator" id="run-ind-' + sid + '">' +
|
|
748
|
+
'<span class="run-pulse"></span>running' +
|
|
749
|
+
'</div>' : '') +
|
|
750
|
+
(actionBtn ? '<div class="kanban-card-actions">' + actionBtn + '</div>' : '') +
|
|
751
|
+
'</div>';
|
|
752
|
+
}
|
|
753
|
+
h += '</div></div>'; // .kanban-col-body + .kanban-col
|
|
754
|
+
}
|
|
755
|
+
h += '</div>'; // .kanban-board
|
|
756
|
+
|
|
757
|
+
el.innerHTML = h;
|
|
758
|
+
wireKanbanDnd();
|
|
759
|
+
refreshOrchestratorStatus();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function getCard(sid) { return document.querySelector('[data-story-id="' + sid + '"]'); }
|
|
763
|
+
|
|
764
|
+
// ── Orchestrator panel ───────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
function _ensureSession(storyId, title) {
|
|
767
|
+
if (_sessions[storyId]) return _sessions[storyId];
|
|
768
|
+
var termEl = document.createElement('div');
|
|
769
|
+
termEl.style.cssText = 'padding:16px 20px;font-family:var(--font-mono);font-size:12px;line-height:1.6;min-height:100%;';
|
|
770
|
+
_sessions[storyId] = { title: title || storyId, termEl, fileOpBuf: [] };
|
|
771
|
+
return _sessions[storyId];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function createPanelTab(storyId, title) {
|
|
775
|
+
_ensureSession(storyId, title);
|
|
776
|
+
var tabs = document.getElementById('orch-tabs');
|
|
777
|
+
if (!tabs) return;
|
|
778
|
+
// Remove placeholder
|
|
779
|
+
var ph = tabs.querySelector('.orch-term-empty');
|
|
780
|
+
if (ph) ph.remove();
|
|
781
|
+
if (document.getElementById('orch-tab-' + storyId)) return;
|
|
782
|
+
var tab = document.createElement('button');
|
|
783
|
+
tab.className = 'orch-tab';
|
|
784
|
+
tab.id = 'orch-tab-' + storyId;
|
|
785
|
+
tab.dataset.storyId = storyId;
|
|
786
|
+
var shortTitle = (title || storyId).slice(0, 20);
|
|
787
|
+
tab.innerHTML =
|
|
788
|
+
'<span class="tab-status-dot starting" id="tdot-' + storyId + '"></span>' +
|
|
789
|
+
'<span>' + esc(shortTitle) + '</span>' +
|
|
790
|
+
'<button class="orch-tab-close" data-sid="' + storyId + '" title="Close">✕</button>';
|
|
791
|
+
tab.onclick = function(e) {
|
|
792
|
+
if (e.target.dataset.sid) { closePanelTab(e.target.dataset.sid); return; }
|
|
793
|
+
activatePanelTab(storyId);
|
|
794
|
+
};
|
|
795
|
+
tabs.appendChild(tab);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function activatePanelTab(storyId) {
|
|
799
|
+
document.querySelectorAll('.orch-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
800
|
+
var tab = document.getElementById('orch-tab-' + storyId);
|
|
801
|
+
if (tab) tab.classList.add('active');
|
|
802
|
+
_panelActive = storyId;
|
|
803
|
+
|
|
804
|
+
var body = document.getElementById('orch-term-body');
|
|
805
|
+
var sess = _sessions[storyId];
|
|
806
|
+
if (body) {
|
|
807
|
+
body.innerHTML = '';
|
|
808
|
+
if (sess) {
|
|
809
|
+
body.appendChild(sess.termEl);
|
|
810
|
+
body.scrollTop = body.scrollHeight;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
var filesEl = document.getElementById('orch-files');
|
|
815
|
+
if (filesEl && sess) {
|
|
816
|
+
filesEl.style.display = sess.fileOpBuf.length ? '' : 'none';
|
|
817
|
+
while (filesEl.children.length > 1) filesEl.removeChild(filesEl.lastChild);
|
|
818
|
+
sess.fileOpBuf.forEach(function(fo) { filesEl.appendChild(_renderFileOpEl(fo)); });
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
var stopBtn = document.getElementById('orch-stop-btn');
|
|
822
|
+
if (stopBtn) stopBtn.style.display = _orchStreams[storyId] ? '' : 'none';
|
|
823
|
+
|
|
824
|
+
var statusEl = document.getElementById('orch-session-status');
|
|
825
|
+
if (statusEl) {
|
|
826
|
+
var running = Object.keys(_orchStreams).length;
|
|
827
|
+
statusEl.textContent = running > 0 ? running + ' running' : '';
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function setTabStatus(storyId, status) {
|
|
832
|
+
var dot = document.getElementById('tdot-' + storyId);
|
|
833
|
+
if (dot) dot.className = 'tab-status-dot ' + (status || 'starting');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function openOrchPanel(storyId) {
|
|
837
|
+
var panel = document.getElementById('orch-panel');
|
|
838
|
+
if (panel) panel.classList.add('open');
|
|
839
|
+
if (storyId && _sessions[storyId]) activatePanelTab(storyId);
|
|
840
|
+
else if (storyId) {
|
|
841
|
+
// Show empty state if no session yet
|
|
842
|
+
var body = document.getElementById('orch-term-body');
|
|
843
|
+
if (body) body.innerHTML = '<div class="orch-term-empty"><div>No output yet for ' + esc(storyId) + '</div></div>';
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function closeOrchPanel() {
|
|
848
|
+
var panel = document.getElementById('orch-panel');
|
|
849
|
+
if (panel) panel.classList.remove('open');
|
|
850
|
+
_panelActive = null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function closePanelTab(storyId) {
|
|
854
|
+
var tab = document.getElementById('orch-tab-' + storyId);
|
|
855
|
+
if (tab) tab.remove();
|
|
856
|
+
delete _sessions[storyId];
|
|
857
|
+
if (_orchStreams[storyId]) { _orchStreams[storyId].close(); delete _orchStreams[storyId]; }
|
|
858
|
+
if (_panelActive === storyId) {
|
|
859
|
+
var remaining = document.querySelectorAll('.orch-tab');
|
|
860
|
+
if (remaining.length > 0) {
|
|
861
|
+
activatePanelTab(remaining[0].dataset.storyId);
|
|
862
|
+
} else {
|
|
863
|
+
var tabs = document.getElementById('orch-tabs');
|
|
864
|
+
if (tabs) tabs.innerHTML = '<div class="orch-term-empty" style="padding:6px 8px;font-size:11px;">No active sessions</div>';
|
|
865
|
+
var body = document.getElementById('orch-term-body');
|
|
866
|
+
if (body) body.innerHTML = '<div class="orch-term-empty"><div>Select a session or run a story card</div></div>';
|
|
867
|
+
closeOrchPanel();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function stopActiveSession() {
|
|
873
|
+
if (_panelActive) stopStory(_panelActive);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function clearActiveTerminal() {
|
|
877
|
+
if (!_panelActive || !_sessions[_panelActive]) return;
|
|
878
|
+
_sessions[_panelActive].termEl.innerHTML = '';
|
|
879
|
+
_sessions[_panelActive].fileOpBuf = [];
|
|
880
|
+
var filesEl = document.getElementById('orch-files');
|
|
881
|
+
if (filesEl) {
|
|
882
|
+
filesEl.style.display = 'none';
|
|
883
|
+
while (filesEl.children.length > 1) filesEl.removeChild(filesEl.lastChild);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function openCleanSessions() {
|
|
888
|
+
fetch(ORCH + '/api/clean-sessions', { method: 'POST',
|
|
889
|
+
headers: { 'Content-Type': 'application/json' },
|
|
890
|
+
body: JSON.stringify({ olderThanDays: 7 }) })
|
|
891
|
+
.then(function(r) { return r.json(); })
|
|
892
|
+
.then(function(d) { showToast('Cleaned ' + (d.removed || 0) + ' sessions'); })
|
|
893
|
+
.catch(function() { showToast('Clean sessions: start orchestrator first'); });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function _renderFileOpEl(fileOp) {
|
|
897
|
+
var div = document.createElement('div');
|
|
898
|
+
div.className = 'kt-file';
|
|
899
|
+
var opClass = fileOp.op === 'write' ? 'op-w' : fileOp.op === 'bash' ? 'op-b' : 'op-r';
|
|
900
|
+
var opLabel = fileOp.op === 'write' ? '✎' : fileOp.op === 'bash' ? '$' : '👁';
|
|
901
|
+
var label = fileOp.path || fileOp.cmd || fileOp.tool;
|
|
902
|
+
div.innerHTML = '<span class="op-icon ' + opClass + '">' + opLabel + '</span> ' + esc(String(label));
|
|
903
|
+
return div;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ── Terminal append (redirect to panel) ─────────────────────────
|
|
907
|
+
|
|
908
|
+
function appendCardChunk(storyId, chunk) {
|
|
909
|
+
var sess = _sessions[storyId];
|
|
910
|
+
if (!sess) return;
|
|
911
|
+
var last = sess.termEl.lastElementChild;
|
|
912
|
+
if (!last || !last.classList.contains('kt-stream')) {
|
|
913
|
+
last = document.createElement('div');
|
|
914
|
+
last.className = 'kt-stream';
|
|
915
|
+
sess.termEl.appendChild(last);
|
|
916
|
+
}
|
|
917
|
+
last.textContent += chunk;
|
|
918
|
+
if (_panelActive === storyId) {
|
|
919
|
+
var body = document.getElementById('orch-term-body');
|
|
920
|
+
if (body) body.scrollTop = body.scrollHeight;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function appendCardLog(storyId, line) {
|
|
925
|
+
var sess = _sessions[storyId];
|
|
926
|
+
if (!sess) return;
|
|
927
|
+
var div = document.createElement('div');
|
|
928
|
+
var cls = 'kt-line';
|
|
929
|
+
if (line.startsWith('⚙')) cls += ' tool';
|
|
930
|
+
else if (line.startsWith('⚠')) cls += ' warn';
|
|
931
|
+
else if (line.startsWith('✗')) cls += ' err';
|
|
932
|
+
else if (line.startsWith('✅')) cls += ' done-line';
|
|
933
|
+
else if (line.startsWith('▶') || line.startsWith('◉') || line.startsWith('■')) cls += ' meta';
|
|
934
|
+
div.className = cls;
|
|
935
|
+
div.textContent = line;
|
|
936
|
+
sess.termEl.appendChild(div);
|
|
937
|
+
if (_panelActive === storyId) {
|
|
938
|
+
var body = document.getElementById('orch-term-body');
|
|
939
|
+
if (body) body.scrollTop = body.scrollHeight;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function appendCardFileOp(storyId, fileOp) {
|
|
944
|
+
var sess = _sessions[storyId];
|
|
945
|
+
if (!sess) return;
|
|
946
|
+
sess.fileOpBuf.push(fileOp);
|
|
947
|
+
if (_panelActive === storyId) {
|
|
948
|
+
var filesEl = document.getElementById('orch-files');
|
|
949
|
+
if (filesEl) { filesEl.style.display = ''; filesEl.appendChild(_renderFileOpEl(fileOp)); }
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ── Run / stop ───────────────────────────────────────────────────
|
|
954
|
+
|
|
955
|
+
function runStory(storyId) {
|
|
956
|
+
if (!storyId) return;
|
|
957
|
+
var card = getCard(storyId);
|
|
958
|
+
var title = card ? (card.querySelector('.kanban-card-title') || {}).textContent : storyId;
|
|
959
|
+
createPanelTab(storyId, title || storyId);
|
|
960
|
+
openOrchPanel(storyId);
|
|
961
|
+
appendCardLog(storyId, '▶ Starting: ' + storyId);
|
|
962
|
+
|
|
963
|
+
fetch(ORCH + '/api/run', {
|
|
964
|
+
method: 'POST',
|
|
965
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') },
|
|
966
|
+
body: JSON.stringify({ storyId }),
|
|
967
|
+
})
|
|
968
|
+
.then(function(r) { return r.json(); })
|
|
969
|
+
.then(function(data) {
|
|
970
|
+
if (data.error) { appendCardLog(storyId, '✗ ' + data.error); setTabStatus(storyId, 'error'); return; }
|
|
971
|
+
appendCardLog(storyId, '▶ pid ' + data.pid);
|
|
972
|
+
setTabStatus(storyId, 'running');
|
|
973
|
+
moveKanbanCard(storyId, 'in_progress');
|
|
974
|
+
connectOrchestratorStream(storyId);
|
|
975
|
+
})
|
|
976
|
+
.catch(function(err) {
|
|
977
|
+
appendCardLog(storyId, '✗ Orchestrator unreachable — ' + err.message);
|
|
978
|
+
appendCardLog(storyId, ' Start with: node server/dashboard.js');
|
|
979
|
+
setTabStatus(storyId, 'error');
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function stopStory(storyId) {
|
|
984
|
+
appendCardLog(storyId, '■ Stopping…');
|
|
985
|
+
fetch(ORCH + '/api/stop', {
|
|
986
|
+
method: 'POST',
|
|
987
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') },
|
|
988
|
+
body: JSON.stringify({ storyId }),
|
|
989
|
+
}).catch(function() {});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ── SSE stream ───────────────────────────────────────────────────
|
|
993
|
+
|
|
994
|
+
function connectOrchestratorStream(storyId) {
|
|
995
|
+
if (_orchStreams[storyId]) _orchStreams[storyId].close();
|
|
996
|
+
var es = new EventSource(ORCH + '/api/stream/' + encodeURIComponent(storyId));
|
|
997
|
+
_orchStreams[storyId] = es;
|
|
998
|
+
|
|
999
|
+
es.onmessage = function(e) {
|
|
1000
|
+
try {
|
|
1001
|
+
var d = JSON.parse(e.data);
|
|
1002
|
+
if (d.chunk) appendCardChunk(storyId, d.chunk);
|
|
1003
|
+
if (d.line) appendCardLog(storyId, d.line);
|
|
1004
|
+
if (d.fileOp) appendCardFileOp(storyId, d.fileOp);
|
|
1005
|
+
if (d.status) {
|
|
1006
|
+
var st = d.status;
|
|
1007
|
+
setTabStatus(storyId, st);
|
|
1008
|
+
if (st === 'done') { appendCardLog(storyId, '✅ Done'); moveKanbanCard(storyId, 'done'); }
|
|
1009
|
+
if (st === 'error') moveKanbanCard(storyId, 'blocked');
|
|
1010
|
+
if (st === 'stopped') appendCardLog(storyId, '■ Stopped');
|
|
1011
|
+
if (st !== 'running') {
|
|
1012
|
+
es.close(); delete _orchStreams[storyId];
|
|
1013
|
+
var stopBtn = document.getElementById('orch-stop-btn');
|
|
1014
|
+
if (stopBtn && _panelActive === storyId) stopBtn.style.display = 'none';
|
|
1015
|
+
_updateOrchDot();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} catch {}
|
|
1019
|
+
};
|
|
1020
|
+
es.onerror = function() {
|
|
1021
|
+
es.close(); delete _orchStreams[storyId];
|
|
1022
|
+
setTabStatus(storyId, 'error');
|
|
1023
|
+
_updateOrchDot();
|
|
1024
|
+
};
|
|
1025
|
+
_updateOrchDot();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function _updateOrchDot() {
|
|
1029
|
+
var running = Object.keys(_orchStreams).length;
|
|
1030
|
+
// Global orch status dot in kanban topbar
|
|
1031
|
+
var dot = document.getElementById('orch-dot');
|
|
1032
|
+
if (dot) {
|
|
1033
|
+
dot.className = 'orch-status-dot' + (running > 0 ? ' up' : '');
|
|
1034
|
+
}
|
|
1035
|
+
// Panel orch dot
|
|
1036
|
+
var pdot = document.getElementById('orch-panel-orch-dot');
|
|
1037
|
+
if (pdot) {
|
|
1038
|
+
pdot.className = 'orch-status-dot' + (running > 0 ? ' up' : '');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function refreshOrchestratorStatus() {
|
|
1043
|
+
fetch(ORCH + '/api/status', { headers: { 'Authorization': 'Bearer ' + (window.__ORCH_TOKEN__ || '') } })
|
|
1044
|
+
.then(function(r) { return r.json(); })
|
|
1045
|
+
.then(function(status) {
|
|
1046
|
+
// Mark orch as reachable
|
|
1047
|
+
var dot = document.getElementById('orch-dot');
|
|
1048
|
+
if (dot && !Object.keys(_orchStreams).length) dot.className = 'orch-status-dot';
|
|
1049
|
+
for (var sid in status) {
|
|
1050
|
+
var info = status[sid];
|
|
1051
|
+
if (info.status === 'running') {
|
|
1052
|
+
moveKanbanCard(sid, 'in_progress');
|
|
1053
|
+
if (!_orchStreams[sid]) {
|
|
1054
|
+
var card = getCard(sid);
|
|
1055
|
+
var title = card ? (card.querySelector('.kanban-card-title') || {}).textContent : sid;
|
|
1056
|
+
createPanelTab(sid, title);
|
|
1057
|
+
connectOrchestratorStream(sid);
|
|
1058
|
+
}
|
|
1059
|
+
} else if (info.status === 'done') {
|
|
1060
|
+
moveKanbanCard(sid, 'done');
|
|
1061
|
+
}
|
|
1062
|
+
// Restore last session logs from /api/status if available
|
|
1063
|
+
if (info.logs && info.logs.length && !_sessions[sid]) {
|
|
1064
|
+
_ensureSession(sid, sid);
|
|
1065
|
+
createPanelTab(sid, sid);
|
|
1066
|
+
info.logs.forEach(function(line) { appendCardLog(sid, line); });
|
|
1067
|
+
setTabStatus(sid, info.status);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
.catch(function() {
|
|
1072
|
+
var dot = document.getElementById('orch-dot');
|
|
1073
|
+
if (dot) dot.className = 'orch-status-dot down';
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ── Move card between columns ────────────────────────────────────
|
|
1078
|
+
|
|
1079
|
+
function moveKanbanCard(storyId, colId) {
|
|
1080
|
+
var card = getCard(storyId);
|
|
1081
|
+
var colBody = document.querySelector('.kanban-col[data-col="' + colId + '"] .kanban-col-body');
|
|
1082
|
+
if (!card || !colBody) return;
|
|
1083
|
+
colBody.appendChild(card);
|
|
1084
|
+
|
|
1085
|
+
// Update card class
|
|
1086
|
+
card.className = card.className.replace(/\bs-\w+\b/g, '').replace(/\brunning\b/g, '').trim();
|
|
1087
|
+
card.classList.add('s-' + colId);
|
|
1088
|
+
if (colId === 'in_progress') card.classList.add('running');
|
|
1089
|
+
|
|
1090
|
+
// Swap action button
|
|
1091
|
+
var actions = card.querySelector('.kanban-card-actions');
|
|
1092
|
+
if (actions) {
|
|
1093
|
+
if (colId === 'in_progress') {
|
|
1094
|
+
actions.innerHTML =
|
|
1095
|
+
'<button class="kanban-stop-btn" data-action="stop">■ Stop</button>' +
|
|
1096
|
+
'<button class="kanban-view-btn" data-action="view">↗ View</button>';
|
|
1097
|
+
} else if (colId === 'done') {
|
|
1098
|
+
actions.innerHTML = '<button class="kanban-view-btn" data-action="view">↗ Logs</button>';
|
|
1099
|
+
} else {
|
|
1100
|
+
actions.innerHTML = '<button class="kanban-run-btn" data-action="run">▶ Run</button>';
|
|
1101
|
+
}
|
|
1102
|
+
wireKanbanCardButtons(card);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Running indicator
|
|
1106
|
+
var ind = card.querySelector('.card-run-indicator');
|
|
1107
|
+
if (colId === 'in_progress' && !ind) {
|
|
1108
|
+
var indEl = document.createElement('div');
|
|
1109
|
+
indEl.className = 'card-run-indicator';
|
|
1110
|
+
indEl.id = 'run-ind-' + storyId;
|
|
1111
|
+
indEl.innerHTML = '<span class="run-pulse"></span>running';
|
|
1112
|
+
card.insertBefore(indEl, actions);
|
|
1113
|
+
} else if (colId !== 'in_progress' && ind) {
|
|
1114
|
+
ind.remove();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
refreshKanbanCounts();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function wireKanbanLogButtons() {} // compat shim
|
|
1121
|
+
|
|
1122
|
+
function wireKanbanCardButtons(card) {
|
|
1123
|
+
var sid = card.dataset.storyId;
|
|
1124
|
+
if (!sid) return;
|
|
1125
|
+
card.querySelectorAll('[data-action="run"]').forEach(function(btn) {
|
|
1126
|
+
btn.onclick = function(e) { e.stopPropagation(); runStory(sid); };
|
|
1127
|
+
});
|
|
1128
|
+
card.querySelectorAll('[data-action="stop"]').forEach(function(btn) {
|
|
1129
|
+
btn.onclick = function(e) { e.stopPropagation(); stopStory(sid); };
|
|
1130
|
+
});
|
|
1131
|
+
card.querySelectorAll('[data-action="view"]').forEach(function(btn) {
|
|
1132
|
+
btn.onclick = function(e) { e.stopPropagation(); openOrchPanel(sid); };
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function wireKanbanDnd() {
|
|
1137
|
+
let dragged = null;
|
|
1138
|
+
document.querySelectorAll('.kanban-card').forEach(card => {
|
|
1139
|
+
wireKanbanCardButtons(card);
|
|
1140
|
+
card.addEventListener('dragstart', e => {
|
|
1141
|
+
if (e.target.tagName === 'BUTTON') { e.preventDefault(); return; }
|
|
1142
|
+
dragged = card; card.style.opacity = '0.5';
|
|
1143
|
+
});
|
|
1144
|
+
card.addEventListener('dragend', () => {
|
|
1145
|
+
dragged = null; if (card) card.style.opacity = '';
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
document.querySelectorAll('.kanban-col-body').forEach(body => {
|
|
1149
|
+
body.addEventListener('dragover', e => { e.preventDefault(); body.classList.add('drag-target'); });
|
|
1150
|
+
body.addEventListener('dragleave', () => body.classList.remove('drag-target'));
|
|
1151
|
+
body.addEventListener('drop', e => {
|
|
1152
|
+
e.preventDefault();
|
|
1153
|
+
body.classList.remove('drag-target');
|
|
1154
|
+
if (!dragged) return;
|
|
1155
|
+
body.appendChild(dragged);
|
|
1156
|
+
dragged.style.opacity = '';
|
|
1157
|
+
refreshKanbanCounts();
|
|
1158
|
+
showToast('Moved (visual only — not persisted)');
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function refreshKanbanCounts() {
|
|
1164
|
+
document.querySelectorAll('.kanban-col').forEach(col => {
|
|
1165
|
+
const n = col.querySelectorAll('.kanban-card').length;
|
|
1166
|
+
const badge = col.querySelector('.kanban-count');
|
|
1167
|
+
if (badge) badge.textContent = n;
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
625
1171
|
// #283: view plan file
|
|
626
1172
|
async function viewPlanFile(phaseId) {
|
|
627
1173
|
// Try to find the plan file via the file tree
|
|
@@ -688,12 +1234,16 @@ function route() {
|
|
|
688
1234
|
// #310: scroll to top on view switch
|
|
689
1235
|
document.querySelector('.content-area')?.scrollTo(0, 0);
|
|
690
1236
|
|
|
1237
|
+
// Close orchestrator panel when leaving kanban — it's fixed-position and overlaps other views
|
|
1238
|
+
if (view !== 'kanban') closeOrchPanel();
|
|
1239
|
+
|
|
691
1240
|
if (view === 'overview') renderOverview();
|
|
692
1241
|
else if (view === 'roadmap') renderRoadmap();
|
|
693
1242
|
else if (view === 'milestones') renderMilestones(subId);
|
|
694
1243
|
else if (view === 'phases') renderPhases(subId);
|
|
695
1244
|
else if (view === 'sprints') renderSprints(subId);
|
|
696
1245
|
else if (view === 'tasks') renderTasks();
|
|
1246
|
+
else if (view === 'kanban') renderKanban();
|
|
697
1247
|
else if (view === 'decisions') renderDecisions();
|
|
698
1248
|
else if (view === 'memory') renderMemory();
|
|
699
1249
|
}
|
|
@@ -720,7 +1270,7 @@ function renderMemory() {
|
|
|
720
1270
|
h += '<div class="filter-bar"><span style="color:var(--text-muted);font-size:var(--text-sm);">Last scanned: ' + esc(m.lastScanned) + '</span></div>';
|
|
721
1271
|
h += '<div id="memory-sections">';
|
|
722
1272
|
for (const [section, files] of Object.entries(sections)) {
|
|
723
|
-
h += '<div
|
|
1273
|
+
h += '<div class="memory-group-header">' + esc(section) + '</div>';
|
|
724
1274
|
h += '<div class="decision-list">';
|
|
725
1275
|
for (const f of files) {
|
|
726
1276
|
const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
|
|
@@ -734,7 +1284,7 @@ function renderMemory() {
|
|
|
734
1284
|
}
|
|
735
1285
|
function listGroup(label, items) {
|
|
736
1286
|
if (!items || !items.length) return '';
|
|
737
|
-
let g = '<div
|
|
1287
|
+
let g = '<div class="memory-group-header">' + esc(label) + ' (' + items.length + ')</div>';
|
|
738
1288
|
g += '<div class="decision-list">';
|
|
739
1289
|
for (const f of items) {
|
|
740
1290
|
g += '<div class="item">' +
|
|
@@ -780,7 +1330,7 @@ function renderDecisions() {
|
|
|
780
1330
|
'<div class="filter-bar"><input class="filter-input" type="text" placeholder="Filter…" oninput="filterItems(this,\\'decisions-inner\\')"></div>' +
|
|
781
1331
|
'<div id="decisions-inner">';
|
|
782
1332
|
for (const [phase, decs] of Object.entries(grouped)) {
|
|
783
|
-
h += '<div
|
|
1333
|
+
h += '<div class="memory-group-header">' + esc(phase) + '</div>';
|
|
784
1334
|
h += '<div class="decision-list">';
|
|
785
1335
|
for (const d of decs) {
|
|
786
1336
|
const title = typeof d === 'string' ? d : (d.title || d.summary || d.decision || JSON.stringify(d).slice(0, 80));
|
|
@@ -814,7 +1364,8 @@ function filterItems(input, listId) {
|
|
|
814
1364
|
const q = input.value.toLowerCase().trim();
|
|
815
1365
|
const el = document.getElementById(listId);
|
|
816
1366
|
if (!el) return;
|
|
817
|
-
|
|
1367
|
+
// Target both list items and agent cards
|
|
1368
|
+
el.querySelectorAll('.item, .agent-card').forEach(item => {
|
|
818
1369
|
item.style.display = !q || item.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
819
1370
|
});
|
|
820
1371
|
}
|
|
@@ -838,12 +1389,12 @@ const _filesPromise = fetch('/api/files').then(function(r) { return r.json(); })
|
|
|
838
1389
|
|
|
839
1390
|
groups.forEach(function(g) {
|
|
840
1391
|
h += '<div class="inline-file-group" style="margin-bottom:var(--space-3);">';
|
|
841
|
-
h += '<div style="font-size:var(--text-xs);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.07em;padding:var(--space-1)
|
|
1392
|
+
h += '<div style="font-size:var(--text-xs);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.07em;padding:var(--space-1) var(--space-3);">' + esc(g.group) + '</div>';
|
|
842
1393
|
if (g.subGroups) {
|
|
843
1394
|
// Render expandable sub-groups (e.g. per-phase)
|
|
844
1395
|
g.subGroups.forEach(function(sg) {
|
|
845
1396
|
h += '<details class="inline-subgroup" open style="margin-left:var(--space-2);margin-bottom:var(--space-1);">';
|
|
846
|
-
h += '<summary style="font-size:var(--text-xs);font-weight:500;color:var(--text-secondary);cursor:pointer;padding:var(--space-1)
|
|
1397
|
+
h += '<summary style="font-size:var(--text-xs);font-weight:500;color:var(--text-secondary);cursor:pointer;padding:var(--space-1) var(--space-3);user-select:none;">' + esc(sg.subGroup) + ' <span style="color:var(--text-muted);font-weight:400;">(' + sg.files.length + ')</span></summary>';
|
|
847
1398
|
sg.files.forEach(function(f) {
|
|
848
1399
|
h += renderFileItem(f, sg.subGroup);
|
|
849
1400
|
});
|
|
@@ -1066,6 +1617,167 @@ function closeSidebar() {
|
|
|
1066
1617
|
// ---- Boot ----
|
|
1067
1618
|
route();
|
|
1068
1619
|
updateTitle();
|
|
1620
|
+
|
|
1621
|
+
// ── xterm Terminal Panel ─────────────────────────────────────────────────────
|
|
1622
|
+
var _term = null;
|
|
1623
|
+
var _termFit = null;
|
|
1624
|
+
var _termEvt = null;
|
|
1625
|
+
var _termStoryId = null;
|
|
1626
|
+
|
|
1627
|
+
function _orchToken() { return window.__ORCH_TOKEN__ || ''; }
|
|
1628
|
+
|
|
1629
|
+
function openTermPanel(storyId, title) {
|
|
1630
|
+
_termStoryId = storyId;
|
|
1631
|
+
document.getElementById('term-title').textContent = title || storyId;
|
|
1632
|
+
document.getElementById('term-panel').classList.add('open');
|
|
1633
|
+
document.getElementById('term-backdrop').classList.add('open');
|
|
1634
|
+
setTermDot('connecting');
|
|
1635
|
+
|
|
1636
|
+
if (!_term && typeof Terminal !== 'undefined') {
|
|
1637
|
+
_term = new Terminal({
|
|
1638
|
+
theme: {
|
|
1639
|
+
background: '#0c0c0e', foreground: '#c9d1d9',
|
|
1640
|
+
cursor: '#58a6ff', selectionBackground: 'rgba(94,106,210,0.25)',
|
|
1641
|
+
black: '#0c0c0e', red: '#ff4444', green: '#3fb950',
|
|
1642
|
+
yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff',
|
|
1643
|
+
cyan: '#39c5cf', white: '#b1bac4', brightBlack: '#6e7681',
|
|
1644
|
+
},
|
|
1645
|
+
fontFamily: '"JetBrains Mono","SF Mono",Consolas,monospace',
|
|
1646
|
+
fontSize: 12, lineHeight: 1.4, convertEol: true,
|
|
1647
|
+
scrollback: 5000, cursorStyle: 'bar',
|
|
1648
|
+
});
|
|
1649
|
+
if (typeof FitAddon !== 'undefined') {
|
|
1650
|
+
_termFit = new FitAddon.FitAddon();
|
|
1651
|
+
_term.loadAddon(_termFit);
|
|
1652
|
+
}
|
|
1653
|
+
_term.open(document.getElementById('term-container'));
|
|
1654
|
+
if (_termFit) _termFit.fit();
|
|
1655
|
+
_term.onData(function(data) {
|
|
1656
|
+
var tok = _orchToken();
|
|
1657
|
+
if (!_termStoryId || !tok) return;
|
|
1658
|
+
fetch('http://localhost:7718/api/message', {
|
|
1659
|
+
method: 'POST',
|
|
1660
|
+
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
1661
|
+
body: JSON.stringify({ storyId: _termStoryId, data: data })
|
|
1662
|
+
}).catch(function() {});
|
|
1663
|
+
});
|
|
1664
|
+
window.addEventListener('resize', function() { if (_termFit) _termFit.fit(); });
|
|
1665
|
+
} else if (_term) {
|
|
1666
|
+
_term.clear();
|
|
1667
|
+
if (_termFit) _termFit.fit();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (_termEvt) { _termEvt.close(); _termEvt = null; }
|
|
1671
|
+
var tok = _orchToken();
|
|
1672
|
+
if (!tok) {
|
|
1673
|
+
if (_term) _term.writeln('\\r\\x1b[31m✗ No orchestrator token — restart the dashboard\\x1b[0m');
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (_term) _term.writeln('\\x1b[90m── connecting to stream: ' + storyId + ' ──\\x1b[0m\\r\\n');
|
|
1678
|
+
|
|
1679
|
+
var url = 'http://localhost:7718/api/stream/' + encodeURIComponent(storyId) + '?token=' + tok;
|
|
1680
|
+
_termEvt = new EventSource(url);
|
|
1681
|
+
_termEvt.onmessage = function(e) {
|
|
1682
|
+
try {
|
|
1683
|
+
var d = JSON.parse(e.data);
|
|
1684
|
+
if (d.line) { if (_term) _term.writeln('\\r' + d.line); }
|
|
1685
|
+
if (d.chunk) { if (_term) _term.write(d.chunk); }
|
|
1686
|
+
if (d.fileOp) { if (_term) _term.writeln('\\r\\x1b[36m[' + d.fileOp.type + '] ' + d.fileOp.path + '\\x1b[0m'); }
|
|
1687
|
+
if (d.status) {
|
|
1688
|
+
setTermDot(d.status);
|
|
1689
|
+
if (d.status === 'done' || d.status === 'error' || d.status === 'stopped') {
|
|
1690
|
+
if (_term) _term.writeln('\\r\\n\\x1b[90m── session ' + d.status + ' ──\\x1b[0m');
|
|
1691
|
+
if (_termEvt) { _termEvt.close(); _termEvt = null; }
|
|
1692
|
+
} else {
|
|
1693
|
+
setTermDot(d.status);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (d.error) { if (_term) _term.writeln('\\r\\x1b[31m✗ ' + d.error + '\\x1b[0m'); }
|
|
1697
|
+
} catch(ex) {}
|
|
1698
|
+
};
|
|
1699
|
+
_termEvt.onerror = function() {
|
|
1700
|
+
if (_term) _term.writeln('\\r\\x1b[31m✗ stream disconnected\\x1b[0m');
|
|
1701
|
+
setTermDot('error');
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function setTermDot(status) {
|
|
1706
|
+
var dot = document.getElementById('term-status-dot');
|
|
1707
|
+
if (dot) dot.className = 'term-status-dot ' + (status || '');
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function closeTermPanel() {
|
|
1711
|
+
document.getElementById('term-panel').classList.remove('open');
|
|
1712
|
+
document.getElementById('term-backdrop').classList.remove('open');
|
|
1713
|
+
if (_termEvt) { _termEvt.close(); _termEvt = null; }
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function termStop() {
|
|
1717
|
+
var tok = _orchToken();
|
|
1718
|
+
if (!_termStoryId || !tok) return;
|
|
1719
|
+
fetch('http://localhost:7718/api/stop', {
|
|
1720
|
+
method: 'POST',
|
|
1721
|
+
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
1722
|
+
body: JSON.stringify({ storyId: _termStoryId })
|
|
1723
|
+
}).catch(function() {});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function termSend() {
|
|
1727
|
+
var inp = document.getElementById('term-input');
|
|
1728
|
+
if (!inp) return;
|
|
1729
|
+
var msg = (inp.value || '').trim();
|
|
1730
|
+
if (!msg) return;
|
|
1731
|
+
inp.value = '';
|
|
1732
|
+
var tok = _orchToken();
|
|
1733
|
+
if (!_termStoryId || !tok) return;
|
|
1734
|
+
if (_term) _term.writeln('\\r\\x1b[90m[you] ' + msg + '\\x1b[0m');
|
|
1735
|
+
fetch('http://localhost:7718/api/message', {
|
|
1736
|
+
method: 'POST',
|
|
1737
|
+
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
1738
|
+
body: JSON.stringify({ storyId: _termStoryId, data: msg + '\\n' })
|
|
1739
|
+
}).catch(function() {});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
document.addEventListener('keydown', function(e) {
|
|
1743
|
+
if (e.key === 'Escape') {
|
|
1744
|
+
var panel = document.getElementById('term-panel');
|
|
1745
|
+
if (panel && panel.classList.contains('open')) closeTermPanel();
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
var _termInputEl = document.getElementById('term-input');
|
|
1750
|
+
if (_termInputEl) {
|
|
1751
|
+
_termInputEl.addEventListener('keydown', function(e) {
|
|
1752
|
+
if (e.key === 'Enter') termSend();
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function runAndOpenTerm(storyId, cmd, title) {
|
|
1757
|
+
var tok = _orchToken();
|
|
1758
|
+
openTermPanel(storyId, title || storyId);
|
|
1759
|
+
if (!tok) return;
|
|
1760
|
+
fetch('http://localhost:7718/api/run', {
|
|
1761
|
+
method: 'POST',
|
|
1762
|
+
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
1763
|
+
body: JSON.stringify({ storyId: storyId, cmd: cmd })
|
|
1764
|
+
}).then(function(r) { return r.json(); })
|
|
1765
|
+
.then(function(data) {
|
|
1766
|
+
if (data.error && data.error !== 'already running') {
|
|
1767
|
+
if (_term) _term.writeln('\\r\\x1b[31m✗ ' + data.error + '\\x1b[0m');
|
|
1768
|
+
} else if (data.error === 'already running') {
|
|
1769
|
+
if (_term) _term.writeln('\\r\\x1b[33m⚠ Already running (pid ' + data.pid + ') — showing live output\\x1b[0m');
|
|
1770
|
+
setTermDot('running');
|
|
1771
|
+
}
|
|
1772
|
+
})
|
|
1773
|
+
.catch(function(err) {
|
|
1774
|
+
if (_term) _term.writeln('\\r\\x1b[31m✗ Orchestrator unreachable: ' + err.message + '\\x1b[0m');
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Also fix existing kanban run/stop to use auth token
|
|
1779
|
+
var _origRunStory = window.runStory;
|
|
1780
|
+
|
|
1069
1781
|
</script>`;
|
|
1070
1782
|
}
|
|
1071
1783
|
|