@hanzlaa/rcode 4.1.2 → 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.
Files changed (67) hide show
  1. package/cli/install.js +176 -13
  2. package/cli/lib/config.cjs +4 -2
  3. package/cli/lib/fsutil.cjs +13 -2
  4. package/cli/lib/homedir.cjs +21 -0
  5. package/cli/lib/schemas.cjs +6 -1
  6. package/cli/nuke.js +13 -8
  7. package/cli/postinstall.js +14 -4
  8. package/cli/rcode-slash-router.cjs +118 -0
  9. package/cli/uninstall.js +59 -1
  10. package/cli/update.js +10 -5
  11. package/dist/rcode.js +234 -230
  12. package/package.json +1 -1
  13. package/server/dashboard.js +26 -7
  14. package/server/lib/api.js +62 -4
  15. package/server/lib/html/client/agents-data.js +22 -18
  16. package/server/lib/html/client/app.js +3 -0
  17. package/server/lib/html/client/components/AgentCard.js +127 -0
  18. package/server/lib/html/client/components/App.js +104 -39
  19. package/server/lib/html/client/components/CommandPalette.js +133 -0
  20. package/server/lib/html/client/components/FileReader.js +116 -0
  21. package/server/lib/html/client/components/FilterChips.js +94 -0
  22. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  23. package/server/lib/html/client/components/OrchPanel.js +80 -52
  24. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  25. package/server/lib/html/client/components/RejectDialog.js +78 -0
  26. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  27. package/server/lib/html/client/components/Sidebar.js +106 -61
  28. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  29. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  30. package/server/lib/html/client/components/Topbar.js +86 -39
  31. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  32. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  33. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  34. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  35. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  36. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  37. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  38. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  39. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  40. package/server/lib/html/client/components/shared.js +47 -11
  41. package/server/lib/html/client/filter-state.js +72 -0
  42. package/server/lib/html/client/icons-client.js +7 -0
  43. package/server/lib/html/client/notify.js +75 -0
  44. package/server/lib/html/client/orchestrator.js +168 -41
  45. package/server/lib/html/client/preact.js +13 -8
  46. package/server/lib/html/client/store.js +70 -6
  47. package/server/lib/html/client/util.js +78 -0
  48. package/server/lib/html/client/vendor/htm.js +1 -0
  49. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  50. package/server/lib/html/client/vendor/preact.js +2 -0
  51. package/server/lib/html/client/views/AgentsView.js +144 -51
  52. package/server/lib/html/client/views/FilesView.js +20 -103
  53. package/server/lib/html/client/views/KanbanView.js +40 -21
  54. package/server/lib/html/client/views/MemoryView.js +26 -9
  55. package/server/lib/html/client/views/MilestonesView.js +4 -4
  56. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  57. package/server/lib/html/client/views/OverviewView.js +47 -239
  58. package/server/lib/html/client/views/PhasesView.js +50 -6
  59. package/server/lib/html/client/views/RoadmapView.js +6 -3
  60. package/server/lib/html/client/views/SprintsView.js +50 -6
  61. package/server/lib/html/client/views/TasksView.js +4 -3
  62. package/server/lib/html/client.js +21 -4
  63. package/server/lib/html/css.js +2761 -8
  64. package/server/lib/html/icons.js +7 -0
  65. package/server/lib/html/shell.js +10 -3
  66. package/server/lib/scanner.js +376 -39
  67. package/server/orchestrator.js +329 -5
@@ -12,18 +12,37 @@
12
12
 
13
13
  import { html, useState, useEffect } from '../preact.js';
14
14
  import { useStore } from '../store.js';
15
- import { stopSession, openTermPanel, runCommandFromUI, ALLOWED_COMMANDS, isSessionRunning } from '../orchestrator.js';
16
- import { orchElapsed } from '../util.js';
15
+ import { stopSession, openTermPanel, ALLOWED_COMMANDS, isSessionRunning, mergeSessionsAndHistory } from '../orchestrator.js';
16
+ import { openRunnerPicker } from '../components/RunnerPicker.js';
17
+ import { RejectDialog } from '../components/RejectDialog.js';
18
+ import { orchElapsed, humanDate } from '../util.js';
17
19
  import { Icon } from '../icons-client.js';
18
20
 
19
21
  // ── Session card ──────────────────────────────────────────────────────────────
20
22
 
23
+ // Tooltip text per session status — shown on the colored status dot.
24
+ const DOT_TITLES = {
25
+ running: 'Running — output streaming',
26
+ blocked: 'Blocked — waiting for your input',
27
+ done: 'Done — exited cleanly',
28
+ exited: 'Exited',
29
+ stopped: 'Stopped',
30
+ error: 'Error',
31
+ };
32
+
21
33
  function OrchCard({ session: s }) {
22
- const running = s.status === 'running';
23
- const waiting = !!s.waiting;
34
+ const blocked = s.status === 'blocked';
35
+ // 'blocked' is a live PTY (server-side classification of running) — keep
36
+ // the Stop button available for it.
37
+ const running = s.status === 'running' || blocked;
38
+ const waiting = blocked || !!s.waiting;
39
+ const [showReject, setShowReject] = useState(false);
24
40
  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);
41
+ const badge = blocked
42
+ ? html`<${Icon} name="alert-triangle" size=${12}/> blocked needs input`
43
+ : waiting ? html`<${Icon} name="hourglass" size=${12}/> waiting for input` : s.status;
44
+ const dotCls = 'term-status-dot ' + (blocked ? 'blocked' : waiting ? 'waiting' : s.status);
45
+ const dotTitle = DOT_TITLES[s.status] || s.status;
27
46
 
28
47
  function handleTerminal(e) {
29
48
  e.stopPropagation();
@@ -38,8 +57,13 @@ function OrchCard({ session: s }) {
38
57
  return html`
39
58
  <div class=${cardCls}>
40
59
  <div class="orch-card-head">
41
- <span class=${dotCls}></span>
60
+ <span class=${dotCls} title=${dotTitle}></span>
42
61
  <span class="orch-card-id">${s.storyId}</span>
62
+ ${s.runner ? html`
63
+ <span class="runner-badge" title=${'Launched with ' + s.runner + (s.model ? ' (' + s.model + ')' : '')}>
64
+ ${s.runner}${s.model ? ' · ' + s.model : ''}
65
+ </span>
66
+ ` : null}
43
67
  <span class="orch-card-badge">${badge}</span>
44
68
  </div>
45
69
  <div class="orch-card-cmd">${s.cmd || ''}</div>
@@ -49,6 +73,11 @@ function OrchCard({ session: s }) {
49
73
  ${' · '}<${Icon} name="eye" size=${12}/> ${s.clients || 0}
50
74
  ${s.pid ? html` · pid ${s.pid}` : null}
51
75
  </div>
76
+ ${s.rejection ? html`
77
+ <div class="orch-card-rejection">
78
+ Rejected: ${s.rejection.reason}
79
+ </div>
80
+ ` : null}
52
81
  <div class="orch-card-actions">
53
82
  <button class="term-run-btn outline" onClick=${handleTerminal}>
54
83
  <${Icon} name="monitor" size=${14}/> Terminal
@@ -56,7 +85,13 @@ function OrchCard({ session: s }) {
56
85
  ${running ? html`
57
86
  <button class="term-run-btn danger" onClick=${handleStop}>■ Stop</button>
58
87
  ` : null}
88
+ ${waiting ? html`
89
+ <button class="term-run-btn danger" onClick=${e => { e.stopPropagation(); setShowReject(true); }}>
90
+ <${Icon} name="alert-triangle" size=${14}/> Reject
91
+ </button>
92
+ ` : null}
59
93
  </div>
94
+ ${showReject ? html`<${RejectDialog} session=${s} onClose=${() => setShowReject(false)}/>` : null}
60
95
  </div>
61
96
  `;
62
97
  }
@@ -65,7 +100,11 @@ function OrchCard({ session: s }) {
65
100
 
66
101
  function sortSessions(sessions) {
67
102
  return [...sessions].sort((a, b) => {
68
- // Waiting-for-input first (needs attention)
103
+ // Blocked-on-input first (needs immediate attention)
104
+ if ((a.status === 'blocked') !== (b.status === 'blocked')) {
105
+ return a.status === 'blocked' ? -1 : 1;
106
+ }
107
+ // Then idle-waiting
69
108
  if (!!a.waiting !== !!b.waiting) return a.waiting ? -1 : 1;
70
109
  // Then running
71
110
  if ((a.status === 'running') !== (b.status === 'running')) {
@@ -84,14 +123,16 @@ function sortSessions(sessions) {
84
123
  * all session and terminal state via runCommandFromUI → runSession.
85
124
  */
86
125
  function CommandRunner() {
87
- useStore(); // subscribe to store updates so isSessionRunning() re-evaluates on each poll
126
+ // Subscribe to store updates so isSessionRunning() + orchOnline re-evaluate on each poll.
127
+ const { orchOnline } = useStore();
88
128
  const [selected, setSelected] = useState(ALLOWED_COMMANDS[0]?.cmd || '');
89
129
  const [busy, setBusy] = useState(false);
90
130
 
91
131
  const slug = selected ? selected.replace(/^\//, '').replace(/\//g, '-') : '';
92
132
  const sessionId = slug ? 'cmd-' + slug : '';
93
133
  const isRunning = sessionId ? isSessionRunning(sessionId) : false;
94
- const disabled = busy || isRunning;
134
+ const orchDown = orchOnline === false;
135
+ const disabled = busy || isRunning || orchDown;
95
136
 
96
137
  // Reset busy 2 s after a Run click — the terminal panel is now open and the
97
138
  // session is streaming. Managed via useEffect so the timer is cancelled if
@@ -102,10 +143,10 @@ function CommandRunner() {
102
143
  return () => clearTimeout(t);
103
144
  }, [busy]);
104
145
 
105
- function handleRun() {
146
+ function handleRun(e) {
106
147
  if (!selected || disabled) return;
107
148
  setBusy(true);
108
- runCommandFromUI(selected);
149
+ openRunnerPicker(e.currentTarget, { kind: 'command', cmd: selected, title: selected });
109
150
  }
110
151
 
111
152
  return html`
@@ -132,12 +173,96 @@ function CommandRunner() {
132
173
  </button>
133
174
  </div>
134
175
  <div class="cmd-runner-hint">
135
- ${isRunning
136
- ? html`Command is runningoutput is streaming to the terminal panel.`
137
- : busy
138
- ? html`Starting — the terminal panel will open shortly.`
139
- : html`Select a command and press Run. Output streams live to the terminal panel.`}
176
+ ${orchDown
177
+ ? html`Orchestrator is unreachablecommands cannot run until it is back.`
178
+ : isRunning
179
+ ? html`Command is running output is streaming to the terminal panel.`
180
+ : busy
181
+ ? html`Starting — the terminal panel will open shortly.`
182
+ : html`Select a command and press Run. Output streams live to the terminal panel.`}
183
+ </div>
184
+ </div>
185
+ `;
186
+ }
187
+
188
+ // ── Run history panel ─────────────────────────────────────────────────────────
189
+
190
+ function durationLabel(ms) {
191
+ if (!ms || !isFinite(ms) || ms <= 0) return '—';
192
+ if (ms < 60000) return Math.round(ms / 1000) + 's';
193
+ if (ms < 3600000) return Math.floor(ms / 60000) + 'm ' + Math.round((ms % 60000) / 1000) + 's';
194
+ return Math.floor(ms / 3600000) + 'h ' + Math.floor((ms % 3600000) / 60000) + 'm';
195
+ }
196
+
197
+ function HistoryRow({ run }) {
198
+ return html`
199
+ <div class="hist-row" key=${run.storyId}>
200
+ <span class=${'term-status-dot ' + run.status}></span>
201
+ <span class="hist-row-id">${run.storyId}</span>
202
+ <span class="hist-row-cmd">${run.cmd}</span>
203
+ <span class="hist-row-duration"><${Icon} name="clock" size=${12}/> ${durationLabel(run.durationMs)}</span>
204
+ <span class="hist-row-status">${run.status}</span>
205
+ </div>
206
+ `;
207
+ }
208
+
209
+ const STATUS_ORDER = ['done', 'exited', 'stopped', 'error'];
210
+
211
+ function HistoryPanel() {
212
+ const { activeSessions, history } = useStore();
213
+ const merged = mergeSessionsAndHistory(activeSessions, history);
214
+ // 'blocked' is a live session (waiting for input), not an ended run.
215
+ const ended = merged.filter(r => r.status !== 'running' && r.status !== 'blocked');
216
+
217
+ if (ended.length === 0) {
218
+ return html`
219
+ <div class="hist-panel">
220
+ <div class="hist-panel-title">
221
+ <${Icon} name="history" size=${16}/> Run History
222
+ </div>
223
+ <div class="empty">No past runs yet.</div>
224
+ </div>
225
+ `;
226
+ }
227
+
228
+ // Group by status (STATUS_ORDER), then within each group by date label
229
+ const byStatus = new Map();
230
+ for (const status of STATUS_ORDER) byStatus.set(status, new Map());
231
+
232
+ for (const run of ended) {
233
+ const bucket = byStatus.get(run.status) || byStatus.get('error');
234
+ const dateKey = humanDate(run.endTime || run.startTime) || 'Unknown date';
235
+ if (!bucket.has(dateKey)) bucket.set(dateKey, []);
236
+ bucket.get(dateKey).push(run);
237
+ }
238
+
239
+ // Sort runs within each date group: newest first
240
+ for (const dateMap of byStatus.values()) {
241
+ for (const runs of dateMap.values()) {
242
+ runs.sort((a, b) => String(b.endTime || b.startTime || '').localeCompare(String(a.endTime || a.startTime || '')));
243
+ }
244
+ }
245
+
246
+ return html`
247
+ <div class="hist-panel">
248
+ <div class="hist-panel-title">
249
+ <${Icon} name="history" size=${16}/> Run History
140
250
  </div>
251
+ ${STATUS_ORDER.map(status => {
252
+ const dateMap = byStatus.get(status);
253
+ if (!dateMap || dateMap.size === 0) return null;
254
+ return html`
255
+ <div class="hist-group" key=${status}>
256
+ <div class="hist-group-title">${status}</div>
257
+ ${[...dateMap.entries()].map(([dateKey, runs]) => html`
258
+ <div key=${dateKey}>
259
+ <div class="hist-date">${dateKey}</div>
260
+ ${runs.map(run => html`<${HistoryRow} key=${run.storyId} run=${run}/>`)}
261
+ </div>
262
+ `)}
263
+ </div>
264
+ `;
265
+ })}
141
266
  </div>
142
267
  `;
143
268
  }
@@ -145,8 +270,9 @@ function CommandRunner() {
145
270
  // ── Root view ─────────────────────────────────────────────────────────────────
146
271
 
147
272
  export function OrchestrationView() {
148
- const { activeSessions } = useStore();
273
+ const { activeSessions, orchOnline } = useStore();
149
274
  const sessions = sortSessions(activeSessions || []);
275
+ const orchDown = orchOnline === false;
150
276
 
151
277
  return html`
152
278
  <div class="view active" id="view-orchestration">
@@ -155,11 +281,18 @@ export function OrchestrationView() {
155
281
  Live agent sessions — run, watch, communicate, stop.
156
282
  </div>
157
283
 
284
+ ${orchDown ? html`
285
+ <div class="orch-down-banner" role="alert">
286
+ ⚠ Orchestrator unreachable (port 7718) — Run buttons are disabled.
287
+ Restart the dashboard, or set ORCH_PORT if the port is in use.
288
+ </div>
289
+ ` : null}
290
+
158
291
  <${CommandRunner}/>
159
292
 
160
293
  ${sessions.length === 0 ? html`
161
294
  <div class="empty">
162
- No active execution.
295
+ ${orchDown ? 'Session status unavailable while the orchestrator is down.' : 'No active execution.'}
163
296
  <div class="empty-action">
164
297
  Use the Command Runner above, or run <code>/rcode-execute</code> to
165
298
  start a phase or sprint.
@@ -172,6 +305,8 @@ export function OrchestrationView() {
172
305
  `)}
173
306
  </div>
174
307
  `}
308
+
309
+ <${HistoryPanel}/>
175
310
  </div>
176
311
  `;
177
312
  }
@@ -1,261 +1,69 @@
1
1
  /**
2
- * OverviewView — Preact component.
2
+ * OverviewView — Preact component (dashboard redesign).
3
3
  *
4
- * Ports renderOverview() from client-render.js to a component tree.
5
- * Reads state via useStore(). Keeps every existing CSS class.
4
+ * Retargeted onto the mockup: a 12-col, 3-row card grid that composes the nine
5
+ * dashboard slot components. The components are empty placeholders for now —
6
+ * other agents fill each one with real content. Layout follows
7
+ * .planning/campaign/MOCKUP-SPEC.md:
8
+ * Row 1: ProgressDonut · CurrentPhase · Timeline
9
+ * Row 2: CompletedTasks · InProgress · Blockers
10
+ * Row 3: RecentDecisions · ProgressTimeline
11
+ *
12
+ * State is read via useStore() and flows down to the slot components as they
13
+ * are filled in; none of them fetch (see DATA-CONTRACT.md).
14
+ *
15
+ * First run: when the server scanned and found no .rcode directory
16
+ * (store.initialized === false) the card grid is replaced by an honest
17
+ * get-started state pointing at /rcode-init — no fabricated dashboard.
6
18
  */
7
19
 
8
20
  import { html } from '../preact.js';
9
21
  import { useStore } from '../store.js';
10
- import { pct, humanDate, allSprints, allTasks, chip, sprintHints as getSprintHints } from '../util.js';
11
- import { ProgressBar, CmdHints, Chip } from '../components/shared.js';
12
- import { Icon } from '../icons-client.js';
13
-
14
- // ---- OverviewView ----
22
+ import { ProgressDonut } from '../components/dashboard/ProgressDonut.js';
23
+ import { CurrentPhase } from '../components/dashboard/CurrentPhase.js';
24
+ import { Timeline } from '../components/dashboard/Timeline.js';
25
+ import { CompletedTasks } from '../components/dashboard/CompletedTasks.js';
26
+ import { InProgress } from '../components/dashboard/InProgress.js';
27
+ import { Blockers } from '../components/dashboard/Blockers.js';
28
+ import { RecentDecisions } from '../components/dashboard/RecentDecisions.js';
29
+ import { ProgressTimeline } from '../components/dashboard/ProgressTimeline.js';
15
30
 
16
31
  export function OverviewView() {
32
+ // Subscribe to the store so this view re-renders on state changes; the slot
33
+ // components will read their slices from it as they are built out.
17
34
  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
- ['/rcode-next', 'What should I do next?'],
32
- ['/rcode-status', 'Quick project status'],
33
- ['/rcode-council','Ask the team a question'],
34
- ];
35
- const sprintHints = getSprintHints(curSprint);
36
- let hints = [...sprintHints, ...baseHints];
37
- if (S.pendingHandoff) {
38
- hints = [['/rcode-resume-work','Resume from the pending handoff'], ...hints];
39
- }
40
35
 
41
- // At-a-glance status tiles — phase, sprint, blocked, last execution.
42
- function StatusSummary() {
43
- const curPhase = (S.phases || []).find(
44
- p => String(p.id) === String(S.currentPhase),
45
- ) || null;
46
- const blockedCount = allTasks(S.phases).filter(t => t.status === 'blocked').length;
47
- const lastExec = S.last_session
48
- ? humanDate(S.last_session.date || S.last_session.timestamp)
49
- : null;
36
+ if (S.initialized === false) {
50
37
  return html`
51
- <div class="stat">
52
- <div class="label">Current Phase</div>
53
- <div class="value">${curPhase ? 'P' + curPhase.id : '—'}</div>
54
- <div class="sub">
55
- ${curPhase ? html`${curPhase.name} · <${Chip} status=${curPhase.status}/>` : 'No phase set'}
38
+ <div id="view-overview" class="view active">
39
+ <div class="firstrun">
40
+ <div class="firstrun-badge" aria-hidden="true">r</div>
41
+ <h2 class="firstrun-title">No project initialized</h2>
42
+ <p class="firstrun-sub">
43
+ This directory has no <code>.rcode</code> project yet, so there is
44
+ no data to show. Initialize one to start tracking phases, sprints,
45
+ and decisions.
46
+ </p>
47
+ <code class="firstrun-cmd">/rcode-init</code>
56
48
  </div>
57
49
  </div>
58
- <div class="stat">
59
- <div class="label">Active Sprint</div>
60
- <div class="value">${curSprint ? curSprint.id : '—'}</div>
61
- <div class="sub">${curSprint ? (curSprint.goal || curSprint.status || 'in progress') : 'No active sprint'}</div>
62
- </div>
63
- <div class="stat" style=${blockedCount ? 'border-left-color:var(--red,#eb5757)' : ''}>
64
- <div class="label">Blocked Tasks</div>
65
- <div class="value" style=${blockedCount ? 'color:var(--red,#eb5757)' : ''}>${blockedCount}</div>
66
- <div class="sub">${blockedCount ? 'needs attention' : 'all clear'}</div>
67
- </div>
68
- <div class="stat">
69
- <div class="label">Last Execution</div>
70
- <div class="value" style="font-size:var(--text-lg,1rem);">${lastExec || '—'}</div>
71
- <div class="sub">${lastExec ? 'most recent session' : 'no sessions yet'}</div>
72
- </div>
73
- `;
74
- }
75
-
76
- // Current sprint progress
77
- function SprintProgress() {
78
- if (!curSprint) return null;
79
- const sts = curSprint.stories || [];
80
- const d = sts.filter(t => t.status === 'done' || t.status === 'completed').length;
81
- return html`
82
- <section>
83
- <h2 class="section-icon"><${Icon} name="zap" size=${16}/> Current Sprint — ${curSprint.id}</h2>
84
- <div class="body">
85
- <div style="margin-bottom:8px;font-size:var(--text-sm);color:var(--text-secondary);">
86
- ${curSprint.goal || ''}
87
- </div>
88
- <div style="display:flex;align-items:center;gap:var(--space-3);">
89
- <div style="flex:1;"><${ProgressBar} done=${d} total=${sts.length}/></div>
90
- <span style="font-size:var(--text-sm);font-weight:600;">
91
- ${d}/${sts.length} (${pct(d, sts.length)})
92
- </span>
93
- </div>
94
- </div>
95
- </section>
96
- `;
97
- }
98
-
99
- // Velocity sparkline (inline SVG, same as client-render.js:267-272)
100
- function VelocitySpark() {
101
- if (!showVelocity) return null;
102
- const vals = completedSprints.map(s => s.velocity_actual);
103
- const max = Math.max(...vals, 1);
104
- const w = 200, h = 40, step = w / (vals.length - 1);
105
- const points = vals.map((v, i) => (i * step) + ',' + (h - (v / max) * h)).join(' ');
106
- return html`
107
- <div class="stat">
108
- <div class="label">Sprint Velocity</div>
109
- <svg width=${w} height=${h + 4} style="margin-top:8px;">
110
- <polyline points=${points} fill="none" stroke="var(--accent-blue)" stroke-width="2"/>
111
- </svg>
112
- <div class="sub">Last ${vals.length} sprints</div>
113
- </div>
114
- `;
115
- }
116
-
117
- // Council sessions
118
- function CouncilSessions() {
119
- if (!Array.isArray(S.council_sessions) || !S.council_sessions.length) return null;
120
- return html`
121
- <section>
122
- <h2 class="section-icon"><${Icon} name="building" size=${16}/> Council Sessions</h2>
123
- <div class="body">
124
- <div class="phase-list">
125
- ${S.council_sessions.slice(-5).reverse().map((cs, i) => html`
126
- <div key=${i} class="item">
127
- <div class="item-title">${cs.topic || cs.title || 'Session'}</div>
128
- <div class="item-meta">
129
- ${cs.date ? humanDate(cs.date) : ''}
130
- ${cs.participants ? ' · ' + cs.participants.join(', ') : ''}
131
- </div>
132
- </div>
133
- `)}
134
- </div>
135
- </div>
136
- </section>
137
- `;
138
- }
139
-
140
- // Chains & workstreams
141
- function ChainsSection() {
142
- if (!chains.length && !workstreams.length) return null;
143
- const { cls, label } = chip('active');
144
- return html`
145
- <section>
146
- <h2 class="section-icon"><${Icon} name="link" size=${16}/> Chains &amp; Workstreams</h2>
147
- <div class="body">
148
- ${chains.length ? html`
149
- <div style="margin-bottom:var(--space-4);">
150
- <strong>Chains</strong>
151
- <div class="phase-list" style="margin-top:var(--space-2);">
152
- ${chains.map((c, i) => html`
153
- <div key=${i} class="item">
154
- <div class="item-title">${c.name || c.id || 'Chain'}</div>
155
- </div>
156
- `)}
157
- </div>
158
- </div>
159
- ` : null}
160
- ${workstreams.length ? html`
161
- <div>
162
- <strong>Workstreams</strong>
163
- <div class="phase-list" style="margin-top:var(--space-2);">
164
- ${workstreams.map((w, i) => {
165
- const wChip = chip(w.status || 'active');
166
- return html`
167
- <div key=${i} class="item">
168
- <div class="item-title">
169
- ${w.name || w.id || 'Workstream'}
170
- ${' '}<span class=${'status-chip ' + wChip.cls}>● ${wChip.label}</span>
171
- </div>
172
- </div>
173
- `;
174
- })}
175
- </div>
176
- </div>
177
- ` : null}
178
- </div>
179
- </section>
180
- `;
181
- }
182
-
183
- // Pending handoff banner
184
- function HandoffBanner() {
185
- if (!S.pendingHandoff) return null;
186
- const ho = S.pendingHandoff;
187
- const when = ho.ts ? humanDate(ho.ts) : '';
188
- const summary = ho.summary ? ' — ' + String(ho.summary).slice(0, 120) : '';
189
- const where = ho.sprint ? ' [sprint ' + ho.sprint + ']' : ho.phase ? ' [phase ' + ho.phase + ']' : '';
190
- return html`
191
- <section style="border-left:4px solid var(--accent-orange,#f59e0b);padding-left:var(--space-3);">
192
- <h2 class="section-icon"><${Icon} name="alert-triangle" size=${16}/> Pending Handoff</h2>
193
- <div class="body">
194
- <div>${when}${where}${summary}</div>
195
- ${ho.resume_hint ? html`
196
- <div style="margin-top:var(--space-2);color:var(--text-secondary);font-size:var(--text-sm);">
197
- ${ho.resume_hint}
198
- </div>
199
- ` : null}
200
- <div style="margin-top:var(--space-3);font-size:var(--text-sm);">
201
- <code>/rcode-resume-work</code>
202
- </div>
203
- </div>
204
- </section>
205
- `;
206
- }
207
-
208
- // Memory bank summary
209
- function MemorySection() {
210
- if (!S.memoryBank || !S.memoryBank.active) return null;
211
- const m = S.memoryBank.active;
212
- return html`
213
- <section
214
- class="item-clickable"
215
- style="cursor:pointer;"
216
- onClick=${() => { location.hash = 'memory'; }}
217
- >
218
- <h2 class="section-icon"><${Icon} name="brain" size=${16}/> Memory Bank →</h2>
219
- <div class="body">
220
- <div class="attr-grid">
221
- <div class="attr-item">
222
- <span class="attr-label">active.md</span>
223
- <span class="attr-value">${m.lines} lines · ${Math.round(m.bytes / 1024 * 10) / 10} KB</span>
224
- </div>
225
- <div class="attr-item">
226
- <span class="attr-label">Updated</span>
227
- <span class="attr-value">${humanDate(m.updated)}</span>
228
- </div>
229
- </div>
230
- </div>
231
- </section>
232
- `;
233
- }
234
-
235
- // Last session line
236
- function LastSession() {
237
- if (!S.last_session) return null;
238
- const ls = S.last_session;
239
- return html`
240
- <span style="color:var(--text-muted);font-size:var(--text-xs);margin-left:var(--space-3);">
241
- Last session: ${humanDate(ls.date || ls.timestamp) || '—'}
242
- </span>
243
50
  `;
244
51
  }
245
52
 
246
53
  return html`
247
54
  <div id="view-overview" class="view active">
248
- <div class="stats">
249
- <${StatusSummary}/>
250
- <${VelocitySpark}/>
55
+ <div class="dash-grid">
56
+ <div class="col-4"><${ProgressDonut}/></div>
57
+ <div class="col-4"><${CurrentPhase}/></div>
58
+ <div class="col-4"><${Timeline}/></div>
59
+
60
+ <div class="col-4"><${CompletedTasks}/></div>
61
+ <div class="col-4"><${InProgress}/></div>
62
+ <div class="col-4"><${Blockers}/></div>
63
+
64
+ <div class="col-6"><${RecentDecisions}/></div>
65
+ <div class="col-6"><${ProgressTimeline}/></div>
251
66
  </div>
252
- <${HandoffBanner}/>
253
- <${SprintProgress}/>
254
- <${MemorySection}/>
255
- <${CouncilSessions}/>
256
- <${ChainsSection}/>
257
- <${LastSession}/>
258
- <${CmdHints} hints=${hints}/>
259
67
  </div>
260
68
  `;
261
69
  }
@@ -9,12 +9,15 @@
9
9
 
10
10
  import { html, useState } from '../preact.js';
11
11
  import { useStore } from '../store.js';
12
- import { pct, humanDate, phaseHints } from '../util.js';
12
+ import { pct, humanDate, phaseHints, chip } from '../util.js';
13
13
  import {
14
14
  Chip, ProgressBar, Breadcrumb, CmdHints, RunningBadge, SprintCard, PhaseCard,
15
15
  } from '../components/shared.js';
16
- import { runAndOpenTerm, openTermPanel, runningInPhase } from '../orchestrator.js';
16
+ import { openTermPanel, runningInPhase } from '../orchestrator.js';
17
+ import { openRunnerPicker } from '../components/RunnerPicker.js';
17
18
  import { Icon } from '../icons-client.js';
19
+ import { StatusSummaryBar } from '../components/StatusSummaryBar.js';
20
+ import { FilterChips } from '../components/FilterChips.js';
18
21
 
19
22
  function AttrItem({ label, value }) {
20
23
  return html`
@@ -60,7 +63,9 @@ function PhaseDetail({ phase: p, S }) {
60
63
 
61
64
  function handleRun(e) {
62
65
  e.stopPropagation();
63
- runAndOpenTerm('phase-' + p.id, '/rcode-execute ' + p.id, 'Phase ' + p.id);
66
+ openRunnerPicker(e.currentTarget, {
67
+ kind: 'session', storyId: 'phase-' + p.id, cmd: '/rcode-execute ' + p.id, title: 'Phase ' + p.id,
68
+ });
64
69
  }
65
70
  function handleTerm(e) {
66
71
  e.stopPropagation();
@@ -117,7 +122,18 @@ function PhaseDetail({ phase: p, S }) {
117
122
  `;
118
123
  }
119
124
 
120
- export function PhasesView({ subId }) {
125
+ /**
126
+ * Map a numeric phase id to its milestone bucket.
127
+ * M1 = phases 1–19, M2 = 20–33, M3 = 34+.
128
+ */
129
+ function phaseMilestone(id) {
130
+ const n = Number(id);
131
+ if (n <= 19) return 'M1';
132
+ if (n <= 33) return 'M2';
133
+ return 'M3';
134
+ }
135
+
136
+ export function PhasesView({ subId, filters }) {
121
137
  const S = useStore();
122
138
  const phases = S.phases || [];
123
139
  const [filter, setFilter] = useState('');
@@ -141,7 +157,22 @@ export function PhasesView({ subId }) {
141
157
  `;
142
158
  }
143
159
 
144
- // List mode
160
+ // List mode — normalise incoming filter prop
161
+ const f = filters || { status: '', milestone: '', date: '' };
162
+
163
+ // Build option lists for FilterChips
164
+ const distinctStatus = [...new Set(phases.map(p => chip(p.status).cls))].filter(Boolean);
165
+ const statusOptions = distinctStatus.map(cls => ({ value: cls, label: cls }));
166
+ const milestoneOptions = [
167
+ { value: 'M1', label: 'M1' },
168
+ { value: 'M2', label: 'M2' },
169
+ { value: 'M3', label: 'M3' },
170
+ ];
171
+ const dateOptions = [
172
+ { value: 'has-completed', label: 'Completed' },
173
+ { value: 'no-completed', label: 'In progress' },
174
+ ];
175
+
145
176
  const allComplete =
146
177
  phases.length > 0 &&
147
178
  phases.every(ph => ph.status === 'complete' || ph.status === 'completed' || ph.status === 'done');
@@ -157,13 +188,26 @@ export function PhasesView({ subId }) {
157
188
  }
158
189
 
159
190
  const q = filter.toLowerCase();
160
- const filtered = q
191
+ let filtered = q
161
192
  ? phases.filter(p => (p.name || '').toLowerCase().includes(q) || String(p.id).includes(q))
162
193
  : phases;
163
194
 
195
+ // Apply chip filters
196
+ if (f.status) filtered = filtered.filter(p => chip(p.status).cls === f.status);
197
+ if (f.milestone) filtered = filtered.filter(p => phaseMilestone(p.id) === f.milestone);
198
+ if (f.date === 'has-completed') filtered = filtered.filter(p => !!p.completed_at);
199
+ if (f.date === 'no-completed') filtered = filtered.filter(p => !p.completed_at);
200
+
164
201
  return html`
165
202
  <div id="view-phases" class="view active">
166
203
  <div class="view-title">Phases</div>
204
+ <${StatusSummaryBar}/>
205
+ <${FilterChips}
206
+ filters=${f}
207
+ statusOptions=${statusOptions}
208
+ milestoneOptions=${milestoneOptions}
209
+ dateOptions=${dateOptions}
210
+ />
167
211
  <div class="filter-bar">
168
212
  <input class="filter-input" type="text" placeholder="Filter…"
169
213
  value=${filter} onInput=${e => setFilter(e.target.value)}/>