@hanzlaa/rcode 4.1.1 → 4.3.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 +1 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +3 -0
- package/cli/agent.js +3 -1
- package/cli/index.js +29 -0
- package/cli/install.js +233 -15
- package/cli/lib/config.cjs +4 -2
- package/cli/lib/fsutil.cjs +13 -2
- package/cli/lib/homedir.cjs +21 -0
- package/cli/lib/schemas.cjs +6 -1
- package/cli/nuke.js +13 -8
- package/cli/postinstall.js +14 -4
- package/cli/rcode-slash-router.cjs +118 -0
- package/cli/uninstall.js +59 -1
- package/cli/update.js +10 -5
- package/cli/workflow.js +3 -1
- package/dist/rcode.js +241 -227
- package/package.json +1 -1
- package/rcode/bin/rcode-tools.cjs +15 -6
- package/rcode/commands/scaffold-project.md +2 -2
- package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/README.md +2 -2
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-09-state-sync.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-code-review/steps/step-02-review.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-git-flow/SKILL.md +1 -1
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +39 -12
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-01-target.md +18 -3
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-02-safety.md +27 -3
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-brownfield.md +57 -0
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-clone.md +4 -1
- package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-04-post-setup.md +15 -1
- package/rcode/skills/actions/4-implementation/rcode-trim/SKILL.md +1 -1
- package/rcode/workflows/audit-milestone.md +1 -1
- package/rcode/workflows/discuss-phase.md +1 -1
- package/rcode/workflows/execute-milestone.md +1 -1
- package/rcode/workflows/execute-regression-gates.md +3 -0
- package/rcode/workflows/execute-sprint.md +27 -1
- package/rcode/workflows/execute-waves.md +6 -0
- package/rcode/workflows/execute.md +13 -3
- package/rcode/workflows/new-milestone.md +2 -2
- package/rcode/workflows/new-project.md +4 -0
- package/rcode/workflows/plan-research-validation.md +1 -1
- package/rcode/workflows/plan-spawn-planner.md +2 -2
- package/rcode/workflows/plan.md +34 -15
- package/rcode/workflows/review.md +2 -0
- package/rcode/workflows/scaffold-project.md +5 -1
- package/rcode/workflows/session-report.md +1 -1
- package/rcode/workflows/ship.md +39 -0
- package/rcode/workflows/sprint-planning.md +27 -0
- package/rcode/workflows/status.md +3 -3
- package/server/dashboard.js +26 -7
- package/server/lib/api.js +62 -4
- package/server/lib/html/client/agents-data.js +22 -18
- package/server/lib/html/client/app.js +3 -0
- package/server/lib/html/client/components/AgentCard.js +127 -0
- package/server/lib/html/client/components/App.js +104 -39
- package/server/lib/html/client/components/CommandPalette.js +133 -0
- package/server/lib/html/client/components/FileReader.js +116 -0
- package/server/lib/html/client/components/FilterChips.js +94 -0
- package/server/lib/html/client/components/NotifyCenter.js +117 -0
- package/server/lib/html/client/components/OrchPanel.js +80 -52
- package/server/lib/html/client/components/PhaseGraph.js +300 -0
- package/server/lib/html/client/components/RejectDialog.js +78 -0
- package/server/lib/html/client/components/RunnerPicker.js +190 -0
- package/server/lib/html/client/components/Sidebar.js +106 -61
- package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
- package/server/lib/html/client/components/TaskPipeline.js +83 -0
- package/server/lib/html/client/components/Topbar.js +86 -39
- package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
- package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
- package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
- package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
- package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
- package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
- package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
- package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
- package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
- package/server/lib/html/client/components/shared.js +47 -11
- package/server/lib/html/client/filter-state.js +72 -0
- package/server/lib/html/client/icons-client.js +7 -0
- package/server/lib/html/client/notify.js +75 -0
- package/server/lib/html/client/orchestrator.js +168 -41
- package/server/lib/html/client/preact.js +13 -8
- package/server/lib/html/client/store.js +70 -6
- package/server/lib/html/client/util.js +78 -0
- package/server/lib/html/client/vendor/htm.js +1 -0
- package/server/lib/html/client/vendor/preact-hooks.js +2 -0
- package/server/lib/html/client/vendor/preact.js +2 -0
- package/server/lib/html/client/views/AgentsView.js +144 -51
- package/server/lib/html/client/views/FilesView.js +20 -103
- package/server/lib/html/client/views/KanbanView.js +40 -21
- package/server/lib/html/client/views/MemoryView.js +26 -9
- package/server/lib/html/client/views/MilestonesView.js +4 -4
- package/server/lib/html/client/views/OrchestrationView.js +154 -19
- package/server/lib/html/client/views/OverviewView.js +47 -239
- package/server/lib/html/client/views/PhasesView.js +50 -6
- package/server/lib/html/client/views/RoadmapView.js +6 -3
- package/server/lib/html/client/views/SprintsView.js +50 -6
- package/server/lib/html/client/views/TasksView.js +4 -3
- package/server/lib/html/client.js +21 -4
- package/server/lib/html/css.js +2761 -8
- package/server/lib/html/icons.js +7 -0
- package/server/lib/html/shell.js +10 -3
- package/server/lib/scanner.js +376 -39
- package/server/orchestrator.js +329 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProjectHealth — progress mini-card used by the Sidebar.
|
|
3
|
+
*
|
|
4
|
+
* Reads `health { pct, label, points[] }` from the store. The scanner now
|
|
5
|
+
* emits real values only: pct is the story-completion percentage (null when
|
|
6
|
+
* nothing is tracked yet — shown as "—"), label is the real blocker count or
|
|
7
|
+
* "Not started", and points exist only when the project has recorded
|
|
8
|
+
* velocity_history. No sample fallback, no invented composite score.
|
|
9
|
+
* See .planning/campaign/DATA-CONTRACT.md. Reads store only — no fetch.
|
|
10
|
+
*
|
|
11
|
+
* No inline style= attributes — all styling via .phealth-* classes in css.js.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html } from '../../preact.js';
|
|
15
|
+
import { useStore } from '../../store.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tone class — driven by the real blocker count, not an invented score.
|
|
19
|
+
* Neutral when nothing is tracked yet.
|
|
20
|
+
*/
|
|
21
|
+
function healthTone(pct, blockerCount) {
|
|
22
|
+
if (pct == null) return 'phealth--none';
|
|
23
|
+
if (blockerCount >= 2) return 'phealth--risk';
|
|
24
|
+
if (blockerCount === 1) return 'phealth--warn';
|
|
25
|
+
return 'phealth--good';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build an SVG polyline `points` string for the sparkline from a value series.
|
|
30
|
+
* Maps values into a 0..W × 0..H box, flipping Y so higher values sit higher.
|
|
31
|
+
*/
|
|
32
|
+
function sparkPoints(points, w, h) {
|
|
33
|
+
if (!points.length) return '';
|
|
34
|
+
const values = points.map(p => Number(p.value) || 0);
|
|
35
|
+
const min = Math.min(...values);
|
|
36
|
+
const max = Math.max(...values);
|
|
37
|
+
const span = max - min || 1;
|
|
38
|
+
const step = points.length > 1 ? w / (points.length - 1) : 0;
|
|
39
|
+
return values
|
|
40
|
+
.map((v, i) => {
|
|
41
|
+
const x = i * step;
|
|
42
|
+
const y = h - ((v - min) / span) * h;
|
|
43
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
44
|
+
})
|
|
45
|
+
.join(' ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ProjectHealth() {
|
|
49
|
+
const S = useStore();
|
|
50
|
+
const health = (S && S.health) || {};
|
|
51
|
+
const blockerCount = Array.isArray(S.blockers) ? S.blockers.length : 0;
|
|
52
|
+
const hasPct = typeof health.pct === 'number';
|
|
53
|
+
const pct = hasPct ? Math.max(0, Math.min(100, Math.round(health.pct))) : null;
|
|
54
|
+
const label = health.label || (hasPct ? '' : 'Not tracked');
|
|
55
|
+
const points = Array.isArray(health.points) ? health.points : [];
|
|
56
|
+
const tone = healthTone(pct, blockerCount);
|
|
57
|
+
|
|
58
|
+
const W = 180;
|
|
59
|
+
const H = 36;
|
|
60
|
+
// Sparkline only from real recorded velocity — 2+ points or nothing.
|
|
61
|
+
const line = points.length >= 2 ? sparkPoints(points, W, H) : '';
|
|
62
|
+
// Close the polygon down to the baseline for a soft area fill.
|
|
63
|
+
const area = line ? `0,${H} ${line} ${W},${H}` : '';
|
|
64
|
+
|
|
65
|
+
return html`
|
|
66
|
+
<section class=${'phealth ' + tone}>
|
|
67
|
+
<p class="phealth-title">Progress</p>
|
|
68
|
+
<div class="phealth-head">
|
|
69
|
+
<span class="phealth-pct">${pct != null ? pct : '—'}${pct != null ? html`<span class="phealth-pct-sign">%</span>` : null}</span>
|
|
70
|
+
<span class="phealth-label">${label}</span>
|
|
71
|
+
</div>
|
|
72
|
+
${line ? html`
|
|
73
|
+
<svg class="phealth-spark" viewBox=${'0 0 ' + W + ' ' + H} preserveAspectRatio="none" aria-hidden="true">
|
|
74
|
+
<polygon class="phealth-spark-area" points=${area} />
|
|
75
|
+
<polyline class="phealth-spark-line" points=${line} />
|
|
76
|
+
</svg>
|
|
77
|
+
` : null}
|
|
78
|
+
</section>
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecentDecisions — Overview redesign, Row 3 Card 1 (Recent Decisions list).
|
|
3
|
+
*
|
|
4
|
+
* "Recent Decisions" card + "View all" link. Each row is a decision title with
|
|
5
|
+
* a status badge (only when the decision actually records a status — no
|
|
6
|
+
* default "Approved") and the date on the right. An empty array renders an
|
|
7
|
+
* honest "No decisions recorded yet" state, never sample data.
|
|
8
|
+
* See .planning/campaign/DATA-CONTRACT.md. Reads props/store only — no fetch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { html } from '../../preact.js';
|
|
12
|
+
import { useStore } from '../../store.js';
|
|
13
|
+
import { humanDate, rowLink } from '../../util.js';
|
|
14
|
+
|
|
15
|
+
// Map a free-form status string to a badge modifier class.
|
|
16
|
+
function statusClass(status) {
|
|
17
|
+
const s = String(status || '').toLowerCase();
|
|
18
|
+
if (s === 'approved') return 'rd-badge--approved';
|
|
19
|
+
if (s === 'rejected') return 'rd-badge--rejected';
|
|
20
|
+
return 'rd-badge--proposed';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function RecentDecisions() {
|
|
24
|
+
const S = useStore();
|
|
25
|
+
const decisions = Array.isArray(S.decisions) ? S.decisions : [];
|
|
26
|
+
|
|
27
|
+
return html`
|
|
28
|
+
<section class="dash-card">
|
|
29
|
+
<div class="rd-head">
|
|
30
|
+
<p class="dash-card-title">Recent Decisions</p>
|
|
31
|
+
<button class="rd-viewall" onClick=${() => { location.hash = 'decisions'; }}>
|
|
32
|
+
View all
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
${decisions.length === 0
|
|
36
|
+
? html`
|
|
37
|
+
<div class="dash-empty">
|
|
38
|
+
<span>No decisions recorded yet</span>
|
|
39
|
+
<code class="dash-empty-hint">/rcode-council</code>
|
|
40
|
+
</div>
|
|
41
|
+
`
|
|
42
|
+
: html`
|
|
43
|
+
<ul class="rd-list">
|
|
44
|
+
${decisions.map((d, i) => html`
|
|
45
|
+
<li class="rd-row ovr-link" key=${d.title + i} ...${rowLink('decisions')}>
|
|
46
|
+
<span class="rd-title">${d.title}</span>
|
|
47
|
+
${d.status
|
|
48
|
+
? html`<span class=${'rd-badge ' + statusClass(d.status)}>${d.status}</span>`
|
|
49
|
+
: html`<span class="rd-status-none">—</span>`}
|
|
50
|
+
<span class="rd-date">${humanDate(d.date) || ''}</span>
|
|
51
|
+
</li>
|
|
52
|
+
`)}
|
|
53
|
+
</ul>
|
|
54
|
+
`}
|
|
55
|
+
</section>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline — Overview redesign, Row 1 Card 3.
|
|
3
|
+
*
|
|
4
|
+
* Target-launch card. With a configured launch date: countdown + a velocity
|
|
5
|
+
* sparkline drawn only from real recorded `velocity_history` + the real
|
|
6
|
+
* open-blocker count. Without one, the dead space becomes a "Milestone
|
|
7
|
+
* outlook": current milestone name, phases done/total, latest recorded
|
|
8
|
+
* velocity, and a hint that launch_date in .rcode/config.yaml enables the
|
|
9
|
+
* countdown — never a projected/invented date.
|
|
10
|
+
*
|
|
11
|
+
* Reads the `timeline { launchDate, onTrack, points[] }` slice plus
|
|
12
|
+
* `blockers`, `milestone`, and `phases` from the store. See DATA-CONTRACT.md.
|
|
13
|
+
* No fetch.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { html } from '../../preact.js';
|
|
17
|
+
import { useStore } from '../../store.js';
|
|
18
|
+
|
|
19
|
+
// SVG viewBox geometry for the sparkline.
|
|
20
|
+
const VW = 280;
|
|
21
|
+
const VH = 88;
|
|
22
|
+
const PAD_X = 8;
|
|
23
|
+
const PAD_Y = 12;
|
|
24
|
+
|
|
25
|
+
/** Days from today until the launch date (>= 0; null when unparseable). */
|
|
26
|
+
function daysUntil(launchDate) {
|
|
27
|
+
const target = new Date(launchDate);
|
|
28
|
+
if (isNaN(target.getTime())) return null;
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const ms = target.setHours(0, 0, 0, 0) - now.setHours(0, 0, 0, 0);
|
|
31
|
+
return Math.max(0, Math.round(ms / 86400000));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Human display of an ISO date, falling back to the raw string. */
|
|
35
|
+
function displayDate(launchDate) {
|
|
36
|
+
const d = new Date(launchDate);
|
|
37
|
+
if (isNaN(d.getTime())) return launchDate;
|
|
38
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Map a value series to evenly-spaced SVG coordinates (y inverted). */
|
|
42
|
+
function toCoords(points) {
|
|
43
|
+
const values = points.map(p => Number(p.value) || 0);
|
|
44
|
+
const max = Math.max(...values, 1);
|
|
45
|
+
const min = Math.min(...values, 0);
|
|
46
|
+
const span = max - min || 1;
|
|
47
|
+
const innerW = VW - PAD_X * 2;
|
|
48
|
+
const innerH = VH - PAD_Y * 2;
|
|
49
|
+
const step = points.length > 1 ? innerW / (points.length - 1) : 0;
|
|
50
|
+
return values.map((v, i) => ({
|
|
51
|
+
x: PAD_X + step * i,
|
|
52
|
+
y: PAD_Y + innerH - ((v - min) / span) * innerH,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Timeline() {
|
|
57
|
+
const S = useStore();
|
|
58
|
+
const timeline = (S && S.timeline) || {};
|
|
59
|
+
const points = Array.isArray(timeline.points) ? timeline.points : [];
|
|
60
|
+
const blockers = Array.isArray(S.blockers) ? S.blockers : [];
|
|
61
|
+
|
|
62
|
+
const hasDate = !!timeline.launchDate;
|
|
63
|
+
const days = hasDate ? daysUntil(timeline.launchDate) : null;
|
|
64
|
+
const daysLine = days == null ? 'Launch scheduled' : `In ${days} day${days === 1 ? '' : 's'}`;
|
|
65
|
+
|
|
66
|
+
let chart = null;
|
|
67
|
+
if (points.length >= 2) {
|
|
68
|
+
const coords = toCoords(points);
|
|
69
|
+
const linePath = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ');
|
|
70
|
+
const areaPath = `${linePath} L ${coords[coords.length - 1].x.toFixed(1)} ${VH - PAD_Y} L ${coords[0].x.toFixed(1)} ${VH - PAD_Y} Z`;
|
|
71
|
+
chart = html`
|
|
72
|
+
<svg class="tl-chart" viewBox=${`0 0 ${VW} ${VH}`} preserveAspectRatio="none" role="img"
|
|
73
|
+
aria-label="Recorded sprint velocity trend">
|
|
74
|
+
<defs>
|
|
75
|
+
<linearGradient id="tlFill" x1="0" y1="0" x2="0" y2="1">
|
|
76
|
+
<stop offset="0%" stop-color="var(--dash-teal)" stop-opacity="0.22"/>
|
|
77
|
+
<stop offset="100%" stop-color="var(--dash-teal)" stop-opacity="0"/>
|
|
78
|
+
</linearGradient>
|
|
79
|
+
</defs>
|
|
80
|
+
<path class="tl-area" d=${areaPath} fill="url(#tlFill)"/>
|
|
81
|
+
<path class="tl-line" d=${linePath} fill="none" stroke="var(--dash-teal)"
|
|
82
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
83
|
+
${coords.map(c => html`
|
|
84
|
+
<circle class="tl-dot" cx=${c.x.toFixed(1)} cy=${c.y.toFixed(1)} r="3"
|
|
85
|
+
fill="var(--dash-bg)" stroke="var(--dash-teal)" stroke-width="2"/>
|
|
86
|
+
`)}
|
|
87
|
+
</svg>
|
|
88
|
+
`;
|
|
89
|
+
} else {
|
|
90
|
+
chart = html`<div class="tl-nochart">${points.length === 1 ? 'Not enough velocity history to chart yet' : 'No velocity history recorded'}</div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const blockerNote = blockers.length
|
|
94
|
+
? `${blockers.length} open blocker${blockers.length === 1 ? '' : 's'}`
|
|
95
|
+
: 'No open blockers';
|
|
96
|
+
|
|
97
|
+
const footer = html`
|
|
98
|
+
<div class="tl-footer">
|
|
99
|
+
<span class=${'tl-status' + (blockers.length ? ' tl-status-risk' : '')}>
|
|
100
|
+
<span class="tl-dot-badge"></span>${blockerNote}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
if (!hasDate) {
|
|
106
|
+
// No launch date configured — show a milestone outlook built from real
|
|
107
|
+
// store data instead of a dead "—" date.
|
|
108
|
+
const phases = Array.isArray(S.phases) ? S.phases : [];
|
|
109
|
+
const phasesDone = phases.filter(p => String(p.state).toLowerCase() === 'done').length;
|
|
110
|
+
const latest = points.length ? points[points.length - 1] : null;
|
|
111
|
+
return html`
|
|
112
|
+
<section class="dash-card tl-card">
|
|
113
|
+
<p class="dash-card-sub tl-label">Milestone Outlook</p>
|
|
114
|
+
<p class="tl-outlook-name">${S.milestone || 'No milestone set'}</p>
|
|
115
|
+
<ul class="tl-outlook">
|
|
116
|
+
<li class="tl-outlook-row">
|
|
117
|
+
<span class="tl-outlook-key">Phases</span>
|
|
118
|
+
<span class="tl-outlook-val">${phases.length ? `${phasesDone}/${phases.length} done` : 'None planned'}</span>
|
|
119
|
+
</li>
|
|
120
|
+
<li class="tl-outlook-row">
|
|
121
|
+
<span class="tl-outlook-key">Latest velocity</span>
|
|
122
|
+
<span class="tl-outlook-val">${latest ? `${latest.value} pts (${latest.label})` : 'Not recorded'}</span>
|
|
123
|
+
</li>
|
|
124
|
+
</ul>
|
|
125
|
+
${chart}
|
|
126
|
+
<p class="tl-outlook-hint">Set <code>launch_date</code> in <code>.rcode/config.yaml</code> to track a launch countdown</p>
|
|
127
|
+
${footer}
|
|
128
|
+
</section>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return html`
|
|
133
|
+
<section class="dash-card tl-card">
|
|
134
|
+
<p class="dash-card-sub tl-label">Target Launch</p>
|
|
135
|
+
<p class="tl-date">${displayDate(timeline.launchDate)}</p>
|
|
136
|
+
<p class="tl-days">${daysLine}</p>
|
|
137
|
+
|
|
138
|
+
${chart}
|
|
139
|
+
|
|
140
|
+
${footer}
|
|
141
|
+
</section>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, useState } from '../preact.js';
|
|
11
|
-
import { pctNum, chip as chipDesc, humanDate, pct } from '../util.js';
|
|
11
|
+
import { pctNum, chip as chipDesc, humanDate, pct, currentPhaseId } from '../util.js';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
isSessionRunning, runningInSprint, runningInPhase,
|
|
14
14
|
} from '../orchestrator.js';
|
|
15
|
+
import { getState } from '../store.js';
|
|
15
16
|
import { Icon } from '../icons-client.js';
|
|
17
|
+
import { TaskPipeline } from './TaskPipeline.js';
|
|
18
|
+
import { openRunnerPicker } from './RunnerPicker.js';
|
|
16
19
|
|
|
17
20
|
// ---- Toast helper (shared by CmdHint copy action and any view) ----
|
|
18
21
|
export function showToast(msg) {
|
|
@@ -23,6 +26,29 @@ export function showToast(msg) {
|
|
|
23
26
|
setTimeout(() => el.classList.remove('show'), 2000);
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
// ---- pressable ----
|
|
30
|
+
/**
|
|
31
|
+
* Spreadable props that make a clickable non-button element keyboard
|
|
32
|
+
* accessible: focusable, announced as a button, activated by Enter/Space.
|
|
33
|
+
* Usage: html`<div class="item item-clickable" ...${pressable(fn)}>…</div>`
|
|
34
|
+
*/
|
|
35
|
+
export function pressable(onActivate) {
|
|
36
|
+
return {
|
|
37
|
+
role: 'button',
|
|
38
|
+
tabindex: 0,
|
|
39
|
+
onClick: onActivate,
|
|
40
|
+
onKeyDown: (e) => {
|
|
41
|
+
// Ignore keydown bubbling up from nested interactive elements
|
|
42
|
+
// (e.g. a Run button inside a clickable card row).
|
|
43
|
+
if (e.target !== e.currentTarget) return;
|
|
44
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
onActivate(e);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
// ---- Chip ----
|
|
27
53
|
/**
|
|
28
54
|
* Status chip.
|
|
@@ -119,7 +145,7 @@ export function CmdHint({ cmd, desc }) {
|
|
|
119
145
|
});
|
|
120
146
|
}
|
|
121
147
|
return html`
|
|
122
|
-
<div class="cmd-hint-item"
|
|
148
|
+
<div class="cmd-hint-item" ...${pressable(handleClick)}>
|
|
123
149
|
<span class="cmd-text">${cmd}</span>
|
|
124
150
|
<span class="cmd-desc">${desc}</span>
|
|
125
151
|
<${Icon} name="copy" size=${14} cls="cmd-copy"/>
|
|
@@ -145,16 +171,22 @@ export function CmdHints({ hints }) {
|
|
|
145
171
|
|
|
146
172
|
// ---- RunBtn ----
|
|
147
173
|
/**
|
|
148
|
-
* Compact run button.
|
|
174
|
+
* Compact run button. Opens the runner/model picker popover anchored to the
|
|
175
|
+
* button; the picker launches the session via runAndOpenTerm.
|
|
149
176
|
* @param {{ storyId: string, cmd: string, label: string }} props
|
|
150
177
|
*/
|
|
151
178
|
export function RunBtn({ storyId, cmd, label }) {
|
|
179
|
+
// Plain read (not a subscription) — every parent view already re-renders
|
|
180
|
+
// on store changes, so the disabled state stays current with the 4s poll.
|
|
181
|
+
const down = getState().orchOnline === false;
|
|
152
182
|
function handleClick(e) {
|
|
153
183
|
e.stopPropagation();
|
|
154
|
-
|
|
184
|
+
openRunnerPicker(e.currentTarget, { kind: 'session', storyId, cmd, title: label });
|
|
155
185
|
}
|
|
156
186
|
return html`
|
|
157
|
-
<button class="card-run-btn"
|
|
187
|
+
<button class="card-run-btn" disabled=${down}
|
|
188
|
+
title=${down ? 'Orchestrator unreachable' : 'Run ' + label}
|
|
189
|
+
onClick=${handleClick}>
|
|
158
190
|
▶ Run
|
|
159
191
|
</button>
|
|
160
192
|
`;
|
|
@@ -180,12 +212,14 @@ export function PhaseCard({ phase: p, S }) {
|
|
|
180
212
|
const sps = p.sprints || [];
|
|
181
213
|
const stories = sps.flatMap(s => s.stories || []);
|
|
182
214
|
const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
183
|
-
|
|
215
|
+
// currentPhase is the contract object (or legacy string) — compare by id.
|
|
216
|
+
const cpId = currentPhaseId(S && S.currentPhase);
|
|
217
|
+
const isCur = cpId !== '' && String(p.id) === cpId;
|
|
184
218
|
const running = runningInPhase(p);
|
|
185
219
|
const borderStyle = isCur ? 'border-left-color:var(--accent-amber)' : '';
|
|
186
220
|
return html`
|
|
187
221
|
<div class=${'item item-clickable'} style=${borderStyle}
|
|
188
|
-
|
|
222
|
+
...${pressable(() => { location.hash = 'phases/' + p.id; })}>
|
|
189
223
|
<div class="item-title">
|
|
190
224
|
${sps.length ? html`<${RunBtn} storyId=${'phase-' + p.id} cmd=${'/rcode-execute ' + p.id} label=${'Phase ' + p.id}/>` : null}
|
|
191
225
|
Phase ${p.id} — ${p.name}
|
|
@@ -232,7 +266,7 @@ export function SprintCard({ sprint: s, S }) {
|
|
|
232
266
|
: '';
|
|
233
267
|
return html`
|
|
234
268
|
<div class=${'item item-clickable' + (isCur ? ' sprint-current' : '')} style=${borderStyle}
|
|
235
|
-
|
|
269
|
+
...${pressable(() => { location.hash = 'sprints/' + s.id; })}>
|
|
236
270
|
<div class="item-title">
|
|
237
271
|
<${RunBtn} storyId=${'sprint-' + s.id} cmd=${'/rcode-execute-sprint ' + s.id} label=${'Sprint ' + s.id}/>
|
|
238
272
|
Sprint ${s.id} — ${s.goal || 'No goal'}
|
|
@@ -290,7 +324,8 @@ export function TaskCard({ task: t }) {
|
|
|
290
324
|
return html`
|
|
291
325
|
<div class="item item-clickable" data-status=${t.status || ''}
|
|
292
326
|
style=${done ? 'opacity:.65' : ''}
|
|
293
|
-
|
|
327
|
+
aria-expanded=${expanded}
|
|
328
|
+
...${pressable(() => setExpanded(e => !e))}>
|
|
294
329
|
<div class="item-title" style=${done ? 'text-decoration:line-through' : ''}>
|
|
295
330
|
${t.id && !done ? html`<${RunBtn} storyId=${t.id} cmd=${'/rcode-dev-story ' + t.id} label=${'Story ' + t.id}/>` : null}
|
|
296
331
|
${done ? '✓ ' : ''}${t.title}
|
|
@@ -302,7 +337,8 @@ export function TaskCard({ task: t }) {
|
|
|
302
337
|
${t.id ? html`<${Tag}>${t.id}</${Tag}>` : null}
|
|
303
338
|
${t.sprintId ? html`<${Tag}>Sprint ${t.sprintId}</${Tag}>` : null}
|
|
304
339
|
${t.phaseId ? html`<${Tag}>Phase ${t.phaseId}</${Tag}>` : null}
|
|
305
|
-
${t.id && running ? html`<span class="run-badge"
|
|
340
|
+
${t.id && running ? html`<span class="run-badge"><span class="live-dot"></span>running</span>` : null}
|
|
341
|
+
<${TaskPipeline} task=${t} running=${t.id && running}/>
|
|
306
342
|
</div>
|
|
307
343
|
${expanded ? html`
|
|
308
344
|
<div class="task-detail">
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filter-state.js — URL hash query-string filter module.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the `?status=&milestone=&date=` filter query portion of
|
|
5
|
+
* location.hash ONLY. It never touches the `view` or `subId` path segments.
|
|
6
|
+
*
|
|
7
|
+
* Hash shape: `#view/subId?status=done&milestone=M3&date=2026-05`
|
|
8
|
+
* - The path segment (`view/subId`) is managed by App.js parseHash.
|
|
9
|
+
* - The query segment (`status=...`) is managed here.
|
|
10
|
+
*
|
|
11
|
+
* Recognised filter keys: `status`, `milestone`, `date`. All others are ignored.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const FILTER_KEYS = ['status', 'milestone', 'date'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse filter state from a raw hash string.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} hash — raw hash string (with or without leading `#`).
|
|
20
|
+
* @returns {{ status: string, milestone: string, date: string }} — each value
|
|
21
|
+
* is a string or `''` when absent. Never throws on malformed input.
|
|
22
|
+
*/
|
|
23
|
+
export function parseFilters(hash) {
|
|
24
|
+
const result = { status: '', milestone: '', date: '' };
|
|
25
|
+
try {
|
|
26
|
+
const raw = typeof hash === 'string' ? hash.replace(/^#/, '') : '';
|
|
27
|
+
const qIdx = raw.indexOf('?');
|
|
28
|
+
if (qIdx === -1) return result;
|
|
29
|
+
const queryStr = raw.slice(qIdx + 1);
|
|
30
|
+
const params = new URLSearchParams(queryStr);
|
|
31
|
+
for (const key of FILTER_KEYS) {
|
|
32
|
+
const val = params.get(key);
|
|
33
|
+
if (val !== null) result[key] = val;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Malformed input — return all-empty object.
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Serialise a filter object into a query string.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ status: string, milestone: string, date: string }} filters
|
|
45
|
+
* @returns {string} — query string WITHOUT a leading `?`. Empty string when no
|
|
46
|
+
* active filter. Keys are always appended in fixed order: status, milestone, date.
|
|
47
|
+
*/
|
|
48
|
+
export function serialiseFilters(filters) {
|
|
49
|
+
const params = new URLSearchParams();
|
|
50
|
+
for (const key of FILTER_KEYS) {
|
|
51
|
+
const val = filters?.[key];
|
|
52
|
+
if (typeof val === 'string' && val !== '') {
|
|
53
|
+
params.append(key, val);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return params.toString();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a full hash body from a view path and filter object.
|
|
61
|
+
*
|
|
62
|
+
* Used by FilterChips (34.2) to update `location.hash` without disturbing
|
|
63
|
+
* the view path segment.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} viewPath — view path segment, e.g. `phases` or `sprints/3`.
|
|
66
|
+
* @param {{ status: string, milestone: string, date: string }} filters
|
|
67
|
+
* @returns {string} — hash body: `viewPath` or `viewPath?query`.
|
|
68
|
+
*/
|
|
69
|
+
export function applyFilters(viewPath, filters) {
|
|
70
|
+
const query = serialiseFilters(filters);
|
|
71
|
+
return query ? `${viewPath}?${query}` : viewPath;
|
|
72
|
+
}
|
|
@@ -34,6 +34,7 @@ export const ICONS = {
|
|
|
34
34
|
minimize: '<line x1="5" y1="14" x2="19" y2="14"/>',
|
|
35
35
|
maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
|
|
36
36
|
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
37
|
+
history: '<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
|
37
38
|
eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
|
|
38
39
|
filePen: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7"/><polyline points="14 2 14 8 20 8"/><path d="M18.4 12.6a2 2 0 0 1 3 3L17 20l-4 1 1-4z"/>',
|
|
39
40
|
hourglass: '<path d="M5 22h14M5 2h14M17 22v-4.17a2 2 0 0 0-.59-1.42L12 12l-4.41 4.41A2 2 0 0 0 7 17.83V22M7 2v4.17a2 2 0 0 0 .59 1.42L12 12l4.41-4.41A2 2 0 0 0 17 6.17V2"/>',
|
|
@@ -54,6 +55,12 @@ export const ICONS = {
|
|
|
54
55
|
// Added in sprint 32.3 — App/Topbar theme toggle icons
|
|
55
56
|
moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
|
|
56
57
|
sun: '<circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.22" y1="4.22" x2="7.05" y2="7.05"/><line x1="16.95" y1="16.95" x2="19.78" y2="19.78"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.22" y1="19.78" x2="7.05" y2="16.95"/><line x1="16.95" y1="7.05" x2="19.78" y2="4.22"/>',
|
|
58
|
+
|
|
59
|
+
// Added in sprint 36.1 — command palette search icon
|
|
60
|
+
search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
|
61
|
+
|
|
62
|
+
// Blocked-session notifications — topbar bell
|
|
63
|
+
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify.js — blocked-session notification tracker.
|
|
3
|
+
*
|
|
4
|
+
* Pure logic, no DOM. The 4s session poll (orchestrator.js _poll) calls
|
|
5
|
+
* trackBlocked(sessions) after each tick. This module diffs the blocked set
|
|
6
|
+
* against the previous tick and writes store.blockedAlerts — the persistent
|
|
7
|
+
* clickable toasts rendered by components/NotifyCenter.js.
|
|
8
|
+
*
|
|
9
|
+
* Browser Notification API is used ONLY when permission is already granted;
|
|
10
|
+
* we never call Notification.requestPermission().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getState, setState } from './store.js';
|
|
14
|
+
|
|
15
|
+
// storyIds that were blocked on the previous poll tick — transition detector.
|
|
16
|
+
let _prevBlocked = new Set();
|
|
17
|
+
|
|
18
|
+
/** True when the browser Notification API is usable without prompting. */
|
|
19
|
+
function notificationsGranted() {
|
|
20
|
+
return typeof window !== 'undefined'
|
|
21
|
+
&& 'Notification' in window
|
|
22
|
+
&& window.Notification.permission === 'granted';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fire a system notification for a newly-blocked session (granted-only). */
|
|
26
|
+
function systemNotify(storyId) {
|
|
27
|
+
if (!notificationsGranted()) return;
|
|
28
|
+
try {
|
|
29
|
+
new window.Notification('Agent waiting for input', {
|
|
30
|
+
body: 'Session ' + storyId + ' is blocked on a question.',
|
|
31
|
+
tag: 'rcode-blocked-' + storyId, // dedupe: re-fires replace, not stack
|
|
32
|
+
});
|
|
33
|
+
} catch { /* notification constructor can throw in some embeds — ignore */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Diff the latest session list against the previous tick:
|
|
38
|
+
* - session newly blocked → append a persistent alert + system notification
|
|
39
|
+
* - session left blocked → drop its alert (answered / exited / stopped)
|
|
40
|
+
* Alerts: [{ storyId, cmd }] in store.blockedAlerts.
|
|
41
|
+
*/
|
|
42
|
+
export function trackBlocked(sessions) {
|
|
43
|
+
const nowBlocked = new Map();
|
|
44
|
+
for (const s of sessions || []) {
|
|
45
|
+
if (s.status === 'blocked') nowBlocked.set(s.storyId, s);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const alerts = (getState().blockedAlerts || [])
|
|
49
|
+
// Drop alerts for sessions that are no longer blocked.
|
|
50
|
+
.filter(a => nowBlocked.has(a.storyId));
|
|
51
|
+
|
|
52
|
+
let changed = alerts.length !== (getState().blockedAlerts || []).length;
|
|
53
|
+
|
|
54
|
+
for (const [storyId, s] of nowBlocked) {
|
|
55
|
+
if (_prevBlocked.has(storyId)) continue; // already known
|
|
56
|
+
if (alerts.some(a => a.storyId === storyId)) continue; // already alerted
|
|
57
|
+
alerts.push({ storyId, cmd: s.cmd || '' });
|
|
58
|
+
systemNotify(storyId);
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_prevBlocked = new Set(nowBlocked.keys());
|
|
63
|
+
if (changed) setState({ blockedAlerts: alerts });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Dismiss one alert toast without touching the session. */
|
|
67
|
+
export function dismissBlockedAlert(storyId) {
|
|
68
|
+
const alerts = (getState().blockedAlerts || []).filter(a => a.storyId !== storyId);
|
|
69
|
+
setState({ blockedAlerts: alerts });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Sessions currently blocked (drives the topbar bell). */
|
|
73
|
+
export function blockedSessions() {
|
|
74
|
+
return (getState().activeSessions || []).filter(s => s.status === 'blocked');
|
|
75
|
+
}
|