@hanzlaa/rcode 3.4.33 → 3.6.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/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 +316 -23
- package/package.json +14 -4
- package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
- package/rihal/agents/rihal-dep-auditor.md +1 -1
- package/rihal/agents/rihal-docs-auditor.md +3 -145
- package/rihal/agents/rihal-i18n-auditor.md +1 -1
- package/rihal/agents/rihal-nyquist-auditor.md +4 -156
- package/rihal/agents/rihal-observability-auditor.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 +2 -2
- 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/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 +154 -5
- package/server/lib/html/client/agents-data.js +27 -0
- package/server/lib/html/client/app.js +15 -0
- package/server/lib/html/client/components/App.js +211 -0
- package/server/lib/html/client/components/OrchPanel.js +293 -0
- package/server/lib/html/client/components/Sidebar.js +73 -0
- package/server/lib/html/client/components/Topbar.js +53 -0
- package/server/lib/html/client/components/XtermPanel.js +220 -0
- package/server/lib/html/client/components/shared.js +330 -0
- package/server/lib/html/client/icons-client.js +85 -0
- package/server/lib/html/client/orchestrator.js +279 -0
- package/server/lib/html/client/preact.js +34 -0
- package/server/lib/html/client/store.js +91 -0
- package/server/lib/html/client/util.js +186 -0
- package/server/lib/html/client/views/AgentsView.js +83 -0
- package/server/lib/html/client/views/DecisionsView.js +102 -0
- package/server/lib/html/client/views/FilesView.js +223 -0
- package/server/lib/html/client/views/KanbanView.js +236 -0
- package/server/lib/html/client/views/MemoryView.js +157 -0
- package/server/lib/html/client/views/MilestonesView.js +136 -0
- package/server/lib/html/client/views/OrchestrationView.js +167 -0
- package/server/lib/html/client/views/OverviewView.js +221 -0
- package/server/lib/html/client/views/PhasesView.js +184 -0
- package/server/lib/html/client/views/RoadmapView.js +238 -0
- package/server/lib/html/client/views/SprintsView.js +178 -0
- package/server/lib/html/client/views/TasksView.js +148 -0
- package/server/lib/html/client.js +42 -1064
- package/server/lib/html/css.js +2266 -466
- package/server/lib/html/icons.js +68 -0
- package/server/lib/html/shell.js +16 -210
- package/server/lib/scanner.js +109 -0
- package/server/orchestrator.js +362 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryView — Preact port of renderMemory() from client-main.js.
|
|
3
|
+
*
|
|
4
|
+
* On mount, fetches /api/memory. Handles three cases:
|
|
5
|
+
* !exists — not initialised empty state
|
|
6
|
+
* !initialised — directory exists but INDEX.md missing
|
|
7
|
+
* populated — sections map + distillates / change records / archive / post-mortems
|
|
8
|
+
*
|
|
9
|
+
* Command hints accordion mirrors the legacy cmdAccordion() output.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html, useState, useEffect } from '../preact.js';
|
|
13
|
+
|
|
14
|
+
// ---- Command hints accordion ----
|
|
15
|
+
const MEMORY_HINTS = [
|
|
16
|
+
['/rcode:memory-init', 'Bootstrap the Memory Bank'],
|
|
17
|
+
['/rcode:memory-update', 'Append a decision, issue, or stakeholder entry'],
|
|
18
|
+
['/rcode:memory-distill', 'Regenerate fast-load distillates'],
|
|
19
|
+
['/rcode:memory-audit', 'Find stale entries and gaps'],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function CmdAccordion({ hints }) {
|
|
23
|
+
const [open, setOpen] = useState(false);
|
|
24
|
+
return html`
|
|
25
|
+
<details open=${open} onToggle=${e => setOpen(e.target.open)} style="margin-top:var(--space-4);">
|
|
26
|
+
<summary style="cursor:pointer;font-size:var(--text-sm);font-weight:600;color:var(--text-secondary);padding:var(--space-2) 0;">
|
|
27
|
+
Useful commands
|
|
28
|
+
</summary>
|
|
29
|
+
<div class="decision-list" style="margin-top:var(--space-2);">
|
|
30
|
+
${hints.map(([cmd, desc]) => html`
|
|
31
|
+
<div class="item" key=${cmd}>
|
|
32
|
+
<div class="item-title"><code>${cmd}</code></div>
|
|
33
|
+
<div class="item-meta">${desc}</div>
|
|
34
|
+
</div>
|
|
35
|
+
`)}
|
|
36
|
+
</div>
|
|
37
|
+
</details>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- Section file list ----
|
|
42
|
+
function SectionGroup({ section, files }) {
|
|
43
|
+
return html`
|
|
44
|
+
<div>
|
|
45
|
+
<div class="memory-group-header">${section}</div>
|
|
46
|
+
<div class="decision-list">
|
|
47
|
+
${files.map(f => {
|
|
48
|
+
const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
|
|
49
|
+
const meta = f.exists ? (f.populated ? 'populated' : 'template only') : 'missing';
|
|
50
|
+
return html`
|
|
51
|
+
<div class="item" key=${f.name}>
|
|
52
|
+
<div class="item-title">${status} ${f.name}</div>
|
|
53
|
+
<div class="item-meta">${meta} · ${f.bytes || 0} bytes</div>
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
})}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- Generic list group (distillates, change records, etc.) ----
|
|
63
|
+
function ListGroup({ label, items }) {
|
|
64
|
+
if (!items || !items.length) return null;
|
|
65
|
+
return html`
|
|
66
|
+
<div>
|
|
67
|
+
<div class="memory-group-header">${label} (${items.length})</div>
|
|
68
|
+
<div class="decision-list">
|
|
69
|
+
${items.map(f => html`
|
|
70
|
+
<div class="item" key=${f.name}>
|
|
71
|
+
<div class="item-title">${f.name}</div>
|
|
72
|
+
</div>
|
|
73
|
+
`)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- Root MemoryView ----
|
|
80
|
+
export function MemoryView() {
|
|
81
|
+
const [memory, setMemory] = useState(null);
|
|
82
|
+
const [loading, setLoading] = useState(true);
|
|
83
|
+
const [error, setError] = useState(null);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setLoading(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
fetch('/api/memory')
|
|
89
|
+
.then(r => r.json())
|
|
90
|
+
.then(data => { setMemory(data); })
|
|
91
|
+
.catch(err => setError(String(err)))
|
|
92
|
+
.finally(() => setLoading(false));
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
if (loading) {
|
|
96
|
+
return html`
|
|
97
|
+
<div class="view active" id="view-memory">
|
|
98
|
+
<div class="view-title">Memory Bank</div>
|
|
99
|
+
<div class="empty">Loading…</div>
|
|
100
|
+
</div>
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (error) {
|
|
105
|
+
return html`
|
|
106
|
+
<div class="view active" id="view-memory">
|
|
107
|
+
<div class="view-title">Memory Bank</div>
|
|
108
|
+
<div class="empty">Failed to load /api/memory: ${error}</div>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!memory || !memory.exists) {
|
|
114
|
+
return html`
|
|
115
|
+
<div class="view active" id="view-memory">
|
|
116
|
+
<div class="view-title">Memory Bank</div>
|
|
117
|
+
<div class="empty">
|
|
118
|
+
<h3 style="color:var(--rihal-gold);">Not initialised</h3>
|
|
119
|
+
<p>The Memory Bank is rcode's structured project context.</p>
|
|
120
|
+
<div class="empty-action">Run <code>/rcode:memory-init</code> to bootstrap</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!memory.initialised) {
|
|
127
|
+
return html`
|
|
128
|
+
<div class="view active" id="view-memory">
|
|
129
|
+
<div class="view-title">Memory Bank</div>
|
|
130
|
+
<div class="empty">
|
|
131
|
+
<p>Directory exists but INDEX.md is missing — re-run <code>/rcode:memory-init</code></p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sections = memory.sections || {};
|
|
138
|
+
|
|
139
|
+
return html`
|
|
140
|
+
<div class="view active" id="view-memory">
|
|
141
|
+
<div class="view-title">Memory Bank</div>
|
|
142
|
+
<div class="filter-bar">
|
|
143
|
+
<span style="color:var(--text-muted);font-size:var(--text-sm);">Last scanned: ${memory.lastScanned || '—'}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div id="memory-sections">
|
|
146
|
+
${Object.entries(sections).map(([section, files]) => html`
|
|
147
|
+
<${SectionGroup} key=${section} section=${section} files=${files} />
|
|
148
|
+
`)}
|
|
149
|
+
<${ListGroup} label="Distillates" items=${memory.distillates} />
|
|
150
|
+
<${ListGroup} label="Change Records" items=${memory.changeRecords} />
|
|
151
|
+
<${ListGroup} label="Milestone Archive" items=${memory.archive} />
|
|
152
|
+
<${ListGroup} label="Post-mortems" items=${memory.postMortems} />
|
|
153
|
+
</div>
|
|
154
|
+
<${CmdAccordion} hints=${MEMORY_HINTS} />
|
|
155
|
+
</div>
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MilestonesView — Preact component.
|
|
3
|
+
*
|
|
4
|
+
* Ports renderMilestones(subId) from client-render.js.
|
|
5
|
+
* List mode: single M1 card with completion ring + summary tags.
|
|
6
|
+
* Detail mode (subId = 'M1'): velocity bars, phase timeline, ring,
|
|
7
|
+
* attr grid, and phase cards.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html } from '../preact.js';
|
|
11
|
+
import { useStore } from '../store.js';
|
|
12
|
+
import { pct, humanDate, allSprints, allTasks } from '../util.js';
|
|
13
|
+
import { CompletionRing, Breadcrumb, Tag, PhaseCard } from '../components/shared.js';
|
|
14
|
+
import { runningTotal } from '../orchestrator.js';
|
|
15
|
+
import { Icon } from '../icons-client.js';
|
|
16
|
+
|
|
17
|
+
function AttrItem({ label, value }) {
|
|
18
|
+
return html`
|
|
19
|
+
<div class="attr-item">
|
|
20
|
+
<span class="attr-label">${label}</span>
|
|
21
|
+
<span class="attr-value">${value}</span>
|
|
22
|
+
</div>
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function VelocityBars({ sprints }) {
|
|
27
|
+
if (!sprints.length) return null;
|
|
28
|
+
const maxV = Math.max(...sprints.map(s => Math.max(s.velocity_actual || 0, s.velocity_target || 0)), 1);
|
|
29
|
+
return html`
|
|
30
|
+
<div>
|
|
31
|
+
<div class="view-title" style="margin-top:var(--space-6)">Velocity History</div>
|
|
32
|
+
<div style="max-width:600px;">
|
|
33
|
+
${sprints.map(s => html`
|
|
34
|
+
<div key=${s.id} class="velocity-bar">
|
|
35
|
+
<div class="velocity-bar-label">S${s.id}</div>
|
|
36
|
+
<div class="velocity-bar-track">
|
|
37
|
+
<div class="velocity-bar-fill" style=${'width:' + ((s.velocity_actual || 0) / maxV * 100) + '%;background:var(--accent-blue);'}></div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="velocity-bar-val">${s.velocity_actual || 0}/${s.velocity_target || '—'}</div>
|
|
40
|
+
</div>
|
|
41
|
+
`)}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function PhaseTimeline({ phases }) {
|
|
48
|
+
const phasesWithDates = phases.filter(p => (p.sprints || []).some(s => s.started_at));
|
|
49
|
+
if (!phasesWithDates.length) return null;
|
|
50
|
+
return html`
|
|
51
|
+
<div>
|
|
52
|
+
<div class="view-title" style="margin-top:var(--space-6)">Phase Timeline</div>
|
|
53
|
+
<div class="phase-list">
|
|
54
|
+
${phasesWithDates.map(p => {
|
|
55
|
+
const sps = p.sprints || [];
|
|
56
|
+
const startDates = sps.map(s => s.started_at).filter(Boolean).sort();
|
|
57
|
+
const endDates = sps.map(s => s.completed_at).filter(Boolean).sort().reverse();
|
|
58
|
+
return html`
|
|
59
|
+
<div key=${p.id} class="item">
|
|
60
|
+
<div class="item-title">P${p.id} — ${p.name}</div>
|
|
61
|
+
<div class="item-meta">
|
|
62
|
+
${startDates[0] ? humanDate(startDates[0]) : '?'} → ${endDates[0] ? humanDate(endDates[0]) : 'ongoing'}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function MilestonesView({ subId }) {
|
|
73
|
+
const S = useStore();
|
|
74
|
+
const phases = S.phases || [];
|
|
75
|
+
const ms = S.milestone || 'M1';
|
|
76
|
+
const total = allTasks(phases);
|
|
77
|
+
const done = total.filter(t => t.status === 'done' || t.status === 'completed');
|
|
78
|
+
|
|
79
|
+
if (subId) {
|
|
80
|
+
const doneP = phases.filter(p => p.status === 'complete' || p.status === 'completed').length;
|
|
81
|
+
const sprints = allSprints(phases).filter(s => s.velocity_actual != null);
|
|
82
|
+
const runningNow = runningTotal();
|
|
83
|
+
|
|
84
|
+
return html`
|
|
85
|
+
<div id="view-milestones" class="view active">
|
|
86
|
+
<${Breadcrumb} items=${[{ label: 'Milestones', hash: 'milestones' }]}/>
|
|
87
|
+
<div class="entity-header">
|
|
88
|
+
<div style="display:flex;align-items:center;gap:var(--space-6);">
|
|
89
|
+
<div>
|
|
90
|
+
<div class="entity-title"><${Icon} name="flag" size=${18}/> ${ms}</div>
|
|
91
|
+
</div>
|
|
92
|
+
<${CompletionRing} done=${done.length} total=${total.length}/>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="attr-grid">
|
|
95
|
+
<${AttrItem} label="Total Phases" value=${phases.length}/>
|
|
96
|
+
<${AttrItem} label="Completed Phases" value=${doneP}/>
|
|
97
|
+
<${AttrItem} label="Current Phase" value=${S.currentPhase || '—'}/>
|
|
98
|
+
<${AttrItem} label="Current Sprint" value=${S.currentSprint || '—'}/>
|
|
99
|
+
<${AttrItem} label="Tasks Done" value=${done.length + '/' + total.length}/>
|
|
100
|
+
<${AttrItem} label="Progress" value=${pct(done.length, total.length)}/>
|
|
101
|
+
<${AttrItem} label="Running now" value=${runningNow}/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<${VelocityBars} sprints=${sprints}/>
|
|
105
|
+
<${PhaseTimeline} phases=${phases}/>
|
|
106
|
+
<div class="view-title" style="margin-top:var(--space-6)">Phases under this milestone</div>
|
|
107
|
+
<div class="phase-list">
|
|
108
|
+
${phases.map(p => html`<${PhaseCard} key=${p.id} phase=${p} S=${S}/>`)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// List mode
|
|
115
|
+
return html`
|
|
116
|
+
<div id="view-milestones" class="view active">
|
|
117
|
+
<div class="view-title">Milestones</div>
|
|
118
|
+
<div class="phase-list">
|
|
119
|
+
<div class="item item-clickable" onClick=${() => { location.hash = 'milestones/M1'; }}>
|
|
120
|
+
<div style="display:flex;align-items:center;gap:var(--space-4);">
|
|
121
|
+
<${CompletionRing} done=${done.length} total=${total.length}/>
|
|
122
|
+
<div>
|
|
123
|
+
<div class="item-title"><${Icon} name="flag" size=${18}/> ${ms}</div>
|
|
124
|
+
<div class="item-meta">
|
|
125
|
+
<${Tag}>${phases.length} phases</${Tag}>
|
|
126
|
+
<${Tag}>${allSprints(phases).length} sprints</${Tag}>
|
|
127
|
+
<${Tag}>${done.length}/${total.length} tasks done</${Tag}>
|
|
128
|
+
<${Tag}>${pct(done.length, total.length)} complete</${Tag}>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrchestrationView — Preact port of renderOrchestration() + _orchCard().
|
|
3
|
+
*
|
|
4
|
+
* Reads activeSessions from the store (kept fresh by startSessionsPoll in
|
|
5
|
+
* orchestrator.js at 4 s intervals). Terminal button sets store.terminal so
|
|
6
|
+
* XtermPanel opens. Stop calls orchestrator.stopSession.
|
|
7
|
+
*
|
|
8
|
+
* No separate poll timer — if a tighter cadence is needed while this view is
|
|
9
|
+
* open, a local useEffect interval can be added. For now the 4 s global poll
|
|
10
|
+
* is sufficient.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { html, useState, useEffect } from '../preact.js';
|
|
14
|
+
import { useStore } from '../store.js';
|
|
15
|
+
import { stopSession, openTermPanel, runCommandFromUI, ALLOWED_COMMANDS, isSessionRunning } from '../orchestrator.js';
|
|
16
|
+
import { orchElapsed } from '../util.js';
|
|
17
|
+
import { Icon } from '../icons-client.js';
|
|
18
|
+
|
|
19
|
+
// ── Session card ──────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function OrchCard({ session: s }) {
|
|
22
|
+
const running = s.status === 'running';
|
|
23
|
+
const waiting = !!s.waiting;
|
|
24
|
+
const cardCls = 'orch-card orch-' + s.status + (waiting ? ' orch-waiting' : '');
|
|
25
|
+
const badge = waiting ? html`<${Icon} name="hourglass" size=${12}/> waiting for input` : s.status;
|
|
26
|
+
const dotCls = 'term-status-dot ' + (waiting ? 'waiting' : s.status);
|
|
27
|
+
|
|
28
|
+
function handleTerminal(e) {
|
|
29
|
+
e.stopPropagation();
|
|
30
|
+
openTermPanel(s.storyId, s.storyId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleStop(e) {
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
stopSession(s.storyId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return html`
|
|
39
|
+
<div class=${cardCls}>
|
|
40
|
+
<div class="orch-card-head">
|
|
41
|
+
<span class=${dotCls}></span>
|
|
42
|
+
<span class="orch-card-id">${s.storyId}</span>
|
|
43
|
+
<span class="orch-card-badge">${badge}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="orch-card-cmd">${s.cmd || ''}</div>
|
|
46
|
+
<div class="orch-card-meta">
|
|
47
|
+
<${Icon} name="clock" size=${12}/> ${orchElapsed(s.startTime)}
|
|
48
|
+
${' · '}<${Icon} name="edit-3" size=${12}/> ${s.filesChanged || 0} file${s.filesChanged === 1 ? '' : 's'}
|
|
49
|
+
${' · '}<${Icon} name="eye" size=${12}/> ${s.clients || 0}
|
|
50
|
+
${s.pid ? html` · pid ${s.pid}` : null}
|
|
51
|
+
</div>
|
|
52
|
+
<div class="orch-card-actions">
|
|
53
|
+
<button class="term-run-btn outline" onClick=${handleTerminal}>
|
|
54
|
+
<${Icon} name="monitor" size=${14}/> Terminal
|
|
55
|
+
</button>
|
|
56
|
+
${running ? html`
|
|
57
|
+
<button class="term-run-btn danger" onClick=${handleStop}>■ Stop</button>
|
|
58
|
+
` : null}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Sorted session list ───────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function sortSessions(sessions) {
|
|
67
|
+
return [...sessions].sort((a, b) => {
|
|
68
|
+
// Waiting-for-input first (needs attention)
|
|
69
|
+
if (!!a.waiting !== !!b.waiting) return a.waiting ? -1 : 1;
|
|
70
|
+
// Then running
|
|
71
|
+
if ((a.status === 'running') !== (b.status === 'running')) {
|
|
72
|
+
return a.status === 'running' ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
// Then most-recent first
|
|
75
|
+
return String(b.startTime || '').localeCompare(String(a.startTime || ''));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Command runner ────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* CommandRunner — dropdown + Run button for launching allowlisted rihal commands.
|
|
83
|
+
* State is local (useState) — no store changes needed; runCommandFromUI handles
|
|
84
|
+
* all session and terminal state via runCommandFromUI → runSession.
|
|
85
|
+
*/
|
|
86
|
+
function CommandRunner() {
|
|
87
|
+
useStore(); // subscribe to store updates so isSessionRunning() re-evaluates on each poll
|
|
88
|
+
const [selected, setSelected] = useState(ALLOWED_COMMANDS[0]?.cmd || '');
|
|
89
|
+
const [busy, setBusy] = useState(false);
|
|
90
|
+
|
|
91
|
+
const slug = selected ? selected.replace(/^\//, '').replace(/\//g, '-') : '';
|
|
92
|
+
const sessionId = slug ? 'cmd-' + slug : '';
|
|
93
|
+
const isRunning = sessionId ? isSessionRunning(sessionId) : false;
|
|
94
|
+
const disabled = busy || isRunning;
|
|
95
|
+
|
|
96
|
+
// Reset busy 2 s after a Run click — the terminal panel is now open and the
|
|
97
|
+
// session is streaming. Managed via useEffect so the timer is cancelled if
|
|
98
|
+
// CommandRunner unmounts before it fires.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!busy) return;
|
|
101
|
+
const t = setTimeout(() => setBusy(false), 2000);
|
|
102
|
+
return () => clearTimeout(t);
|
|
103
|
+
}, [busy]);
|
|
104
|
+
|
|
105
|
+
function handleRun() {
|
|
106
|
+
if (!selected || disabled) return;
|
|
107
|
+
setBusy(true);
|
|
108
|
+
runCommandFromUI(selected);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return html`
|
|
112
|
+
<div class="cmd-runner">
|
|
113
|
+
<div class="cmd-runner-title">
|
|
114
|
+
<${Icon} name="terminal" size=${14}/> Command Runner
|
|
115
|
+
</div>
|
|
116
|
+
<div class="cmd-runner-row">
|
|
117
|
+
<select class="cmd-runner-select"
|
|
118
|
+
value=${selected}
|
|
119
|
+
onChange=${e => setSelected(e.target.value)}>
|
|
120
|
+
${ALLOWED_COMMANDS.map(({ cmd, label }) => html`
|
|
121
|
+
<option key=${cmd} value=${cmd}>${label}</option>
|
|
122
|
+
`)}
|
|
123
|
+
</select>
|
|
124
|
+
<button class=${'cmd-runner-btn' + (disabled ? ' cmd-runner-btn--busy' : '')}
|
|
125
|
+
onClick=${handleRun}
|
|
126
|
+
disabled=${disabled}>
|
|
127
|
+
${isRunning
|
|
128
|
+
? html`<${Icon} name="hourglass" size=${14}/> Running…`
|
|
129
|
+
: busy
|
|
130
|
+
? html`<${Icon} name="hourglass" size=${14}/> Starting…`
|
|
131
|
+
: html`<${Icon} name="play" size=${14}/> Run`}
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Root view ─────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export function OrchestrationView() {
|
|
141
|
+
const { activeSessions } = useStore();
|
|
142
|
+
const sessions = sortSessions(activeSessions || []);
|
|
143
|
+
|
|
144
|
+
return html`
|
|
145
|
+
<div class="view active" id="view-orchestration">
|
|
146
|
+
<div class="view-title section-icon"><${Icon} name="activity" size=${18}/> Orchestration</div>
|
|
147
|
+
<div class="orch-subtitle">
|
|
148
|
+
Live agent sessions — run, watch, communicate, stop.
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<${CommandRunner}/>
|
|
152
|
+
|
|
153
|
+
${sessions.length === 0 ? html`
|
|
154
|
+
<div class="empty">
|
|
155
|
+
No agent sessions yet.
|
|
156
|
+
<div class="empty-action">Run a phase or sprint to start one</div>
|
|
157
|
+
</div>
|
|
158
|
+
` : html`
|
|
159
|
+
<div class="orch-grid">
|
|
160
|
+
${sessions.map(s => html`
|
|
161
|
+
<${OrchCard} key=${s.storyId} session=${s} />
|
|
162
|
+
`)}
|
|
163
|
+
</div>
|
|
164
|
+
`}
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OverviewView — Preact component.
|
|
3
|
+
*
|
|
4
|
+
* Ports renderOverview() from client-render.js to a component tree.
|
|
5
|
+
* Reads state via useStore(). Keeps every existing CSS class.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html } from '../preact.js';
|
|
9
|
+
import { useStore } from '../store.js';
|
|
10
|
+
import { pct, humanDate, allSprints, chip, sprintHints as getSprintHints } from '../util.js';
|
|
11
|
+
import { ProgressBar, CmdHints } from '../components/shared.js';
|
|
12
|
+
import { Icon } from '../icons-client.js';
|
|
13
|
+
|
|
14
|
+
// ---- OverviewView ----
|
|
15
|
+
|
|
16
|
+
export function OverviewView() {
|
|
17
|
+
const S = useStore();
|
|
18
|
+
const sprints = allSprints(S.phases);
|
|
19
|
+
const curSprint = sprints.find(s => s.id === S.currentSprint) || null;
|
|
20
|
+
|
|
21
|
+
// Velocity sparkline data
|
|
22
|
+
const completedSprints = sprints.filter(s => s.velocity_actual != null);
|
|
23
|
+
const showVelocity = completedSprints.length > 1;
|
|
24
|
+
|
|
25
|
+
// Chains & workstreams
|
|
26
|
+
const chains = S.chains || [];
|
|
27
|
+
const workstreams = S.workstreams || [];
|
|
28
|
+
|
|
29
|
+
// Cmd hints
|
|
30
|
+
const baseHints = [
|
|
31
|
+
['/rihal-next', 'What should I do next?'],
|
|
32
|
+
['/rihal-status', 'Quick project status'],
|
|
33
|
+
['/rihal-council','Ask the team a question'],
|
|
34
|
+
];
|
|
35
|
+
const sprintHints = getSprintHints(curSprint);
|
|
36
|
+
let hints = [...sprintHints, ...baseHints];
|
|
37
|
+
if (S.pendingHandoff) {
|
|
38
|
+
hints = [['/rihal-resume-work','Resume from the pending handoff'], ...hints];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Current sprint progress
|
|
42
|
+
function SprintProgress() {
|
|
43
|
+
if (!curSprint) return null;
|
|
44
|
+
const sts = curSprint.stories || [];
|
|
45
|
+
const d = sts.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
46
|
+
return html`
|
|
47
|
+
<section>
|
|
48
|
+
<h2 class="section-icon"><${Icon} name="zap" size=${16}/> Current Sprint — ${curSprint.id}</h2>
|
|
49
|
+
<div class="body">
|
|
50
|
+
<div style="margin-bottom:8px;font-size:var(--text-sm);color:var(--text-secondary);">
|
|
51
|
+
${curSprint.goal || ''}
|
|
52
|
+
</div>
|
|
53
|
+
<div style="display:flex;align-items:center;gap:var(--space-3);">
|
|
54
|
+
<div style="flex:1;"><${ProgressBar} done=${d} total=${sts.length}/></div>
|
|
55
|
+
<span style="font-size:var(--text-sm);font-weight:600;">
|
|
56
|
+
${d}/${sts.length} (${pct(d, sts.length)})
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Velocity sparkline (inline SVG, same as client-render.js:267-272)
|
|
65
|
+
function VelocitySpark() {
|
|
66
|
+
if (!showVelocity) return null;
|
|
67
|
+
const vals = completedSprints.map(s => s.velocity_actual);
|
|
68
|
+
const max = Math.max(...vals, 1);
|
|
69
|
+
const w = 200, h = 40, step = w / (vals.length - 1);
|
|
70
|
+
const points = vals.map((v, i) => (i * step) + ',' + (h - (v / max) * h)).join(' ');
|
|
71
|
+
return html`
|
|
72
|
+
<div class="stat">
|
|
73
|
+
<div class="label">Sprint Velocity</div>
|
|
74
|
+
<svg width=${w} height=${h + 4} style="margin-top:8px;">
|
|
75
|
+
<polyline points=${points} fill="none" stroke="var(--accent-blue)" stroke-width="2"/>
|
|
76
|
+
</svg>
|
|
77
|
+
<div class="sub">Last ${vals.length} sprints</div>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Council sessions
|
|
83
|
+
function CouncilSessions() {
|
|
84
|
+
if (!Array.isArray(S.council_sessions) || !S.council_sessions.length) return null;
|
|
85
|
+
return html`
|
|
86
|
+
<section>
|
|
87
|
+
<h2 class="section-icon"><${Icon} name="building" size=${16}/> Council Sessions</h2>
|
|
88
|
+
<div class="body">
|
|
89
|
+
<div class="phase-list">
|
|
90
|
+
${S.council_sessions.slice(-5).reverse().map((cs, i) => html`
|
|
91
|
+
<div key=${i} class="item">
|
|
92
|
+
<div class="item-title">${cs.topic || cs.title || 'Session'}</div>
|
|
93
|
+
<div class="item-meta">
|
|
94
|
+
${cs.date ? humanDate(cs.date) : ''}
|
|
95
|
+
${cs.participants ? ' · ' + cs.participants.join(', ') : ''}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Chains & workstreams
|
|
106
|
+
function ChainsSection() {
|
|
107
|
+
if (!chains.length && !workstreams.length) return null;
|
|
108
|
+
const { cls, label } = chip('active');
|
|
109
|
+
return html`
|
|
110
|
+
<section>
|
|
111
|
+
<h2 class="section-icon"><${Icon} name="link" size=${16}/> Chains & Workstreams</h2>
|
|
112
|
+
<div class="body">
|
|
113
|
+
${chains.length ? html`
|
|
114
|
+
<div style="margin-bottom:var(--space-4);">
|
|
115
|
+
<strong>Chains</strong>
|
|
116
|
+
<div class="phase-list" style="margin-top:var(--space-2);">
|
|
117
|
+
${chains.map((c, i) => html`
|
|
118
|
+
<div key=${i} class="item">
|
|
119
|
+
<div class="item-title">${c.name || c.id || 'Chain'}</div>
|
|
120
|
+
</div>
|
|
121
|
+
`)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
` : null}
|
|
125
|
+
${workstreams.length ? html`
|
|
126
|
+
<div>
|
|
127
|
+
<strong>Workstreams</strong>
|
|
128
|
+
<div class="phase-list" style="margin-top:var(--space-2);">
|
|
129
|
+
${workstreams.map((w, i) => {
|
|
130
|
+
const wChip = chip(w.status || 'active');
|
|
131
|
+
return html`
|
|
132
|
+
<div key=${i} class="item">
|
|
133
|
+
<div class="item-title">
|
|
134
|
+
${w.name || w.id || 'Workstream'}
|
|
135
|
+
${' '}<span class=${'status-chip ' + wChip.cls}>● ${wChip.label}</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
` : null}
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Pending handoff banner
|
|
149
|
+
function HandoffBanner() {
|
|
150
|
+
if (!S.pendingHandoff) return null;
|
|
151
|
+
const ho = S.pendingHandoff;
|
|
152
|
+
const when = ho.ts ? humanDate(ho.ts) : '';
|
|
153
|
+
const summary = ho.summary ? ' — ' + String(ho.summary).slice(0, 120) : '';
|
|
154
|
+
const where = ho.sprint ? ' [sprint ' + ho.sprint + ']' : ho.phase ? ' [phase ' + ho.phase + ']' : '';
|
|
155
|
+
return html`
|
|
156
|
+
<section style="border-left:4px solid var(--accent-orange,#f59e0b);padding-left:var(--space-3);">
|
|
157
|
+
<h2 class="section-icon"><${Icon} name="alert-triangle" size=${16}/> Pending Handoff</h2>
|
|
158
|
+
<div class="body">
|
|
159
|
+
<div>${when}${where}${summary}</div>
|
|
160
|
+
${ho.resume_hint ? html`
|
|
161
|
+
<div style="margin-top:var(--space-2);color:var(--text-secondary);font-size:var(--text-sm);">
|
|
162
|
+
${ho.resume_hint}
|
|
163
|
+
</div>
|
|
164
|
+
` : null}
|
|
165
|
+
<div style="margin-top:var(--space-3);font-size:var(--text-sm);">
|
|
166
|
+
<code>/rihal-resume-work</code>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Memory bank summary
|
|
174
|
+
function MemorySection() {
|
|
175
|
+
if (!S.memoryBank || !S.memoryBank.active) return null;
|
|
176
|
+
const m = S.memoryBank.active;
|
|
177
|
+
return html`
|
|
178
|
+
<section>
|
|
179
|
+
<h2 class="section-icon"><${Icon} name="brain" size=${16}/> Memory Bank</h2>
|
|
180
|
+
<div class="body">
|
|
181
|
+
<div class="attr-grid">
|
|
182
|
+
<div class="attr-item">
|
|
183
|
+
<span class="attr-label">active.md</span>
|
|
184
|
+
<span class="attr-value">${m.lines} lines · ${Math.round(m.bytes / 1024 * 10) / 10} KB</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="attr-item">
|
|
187
|
+
<span class="attr-label">Updated</span>
|
|
188
|
+
<span class="attr-value">${humanDate(m.updated)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</section>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Last session line
|
|
197
|
+
function LastSession() {
|
|
198
|
+
if (!S.last_session) return null;
|
|
199
|
+
const ls = S.last_session;
|
|
200
|
+
return html`
|
|
201
|
+
<span style="color:var(--text-muted);font-size:var(--text-xs);margin-left:var(--space-3);">
|
|
202
|
+
Last session: ${humanDate(ls.date || ls.timestamp) || '—'}
|
|
203
|
+
</span>
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return html`
|
|
208
|
+
<div id="view-overview" class="view active">
|
|
209
|
+
<div class="stats">
|
|
210
|
+
<${VelocitySpark}/>
|
|
211
|
+
</div>
|
|
212
|
+
<${HandoffBanner}/>
|
|
213
|
+
<${SprintProgress}/>
|
|
214
|
+
<${MemorySection}/>
|
|
215
|
+
<${CouncilSessions}/>
|
|
216
|
+
<${ChainsSection}/>
|
|
217
|
+
<${LastSession}/>
|
|
218
|
+
<${CmdHints} hints=${hints}/>
|
|
219
|
+
</div>
|
|
220
|
+
`;
|
|
221
|
+
}
|