@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
@@ -2,46 +2,17 @@
2
2
  * FilesView — Preact port of the Files view from client-main.js.
3
3
  *
4
4
  * On mount: fetches /api/files to build the grouped file tree.
5
- * Clicking a file: fetches /api/file?path=... and renders markdown via the
6
- * global `marked` CDN lib (stays a CDN global unchanged from legacy).
5
+ * Clicking a file: opens the shared FileReader slide-over, which fetches
6
+ * /api/file?path=... and renders markdown via the global `marked` CDN lib.
7
7
  *
8
8
  * Agent-jump bridge: when the store field `requestedFile` is set (by
9
9
  * AgentsView), FilesView picks it up and pre-fills the search filter, then
10
10
  * clears the field so subsequent renders don't re-trigger.
11
11
  */
12
12
 
13
- import { html, useState, useEffect, useCallback } from '../preact.js';
13
+ import { html, useState, useEffect } from '../preact.js';
14
14
  import { useStore, setState } from '../store.js';
15
- import { showToast } from '../components/shared.js';
16
-
17
- // ---- Markdown helpers (ported from client-main.js:287-294) ----
18
- function stripFrontmatter(md) {
19
- if (!md.startsWith('---')) return md;
20
- const end = md.indexOf('\n---', 3);
21
- return end === -1 ? md : md.slice(end + 4).trimStart();
22
- }
23
-
24
- // Minimal HTML sanitizer for rendered markdown. No DOMPurify dependency on the
25
- // client, so we strip the dangerous primitives via regex after marked emits
26
- // HTML: script/iframe/object/embed tags, inline event handlers, and
27
- // javascript:/data: URLs in href/src. Markdown content comes from the project
28
- // dir (semi-trusted) but may include attacker-controlled text checked into a
29
- // repo, so we cannot trust raw HTML passthrough.
30
- function sanitizeHtml(html) {
31
- return String(html)
32
- .replace(/<\s*(script|iframe|object|embed|link|meta|style)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, '')
33
- .replace(/<\s*(script|iframe|object|embed|link|meta|style)\b[^>]*\/?>/gi, '')
34
- .replace(/\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
35
- .replace(/(href|src|xlink:href)\s*=\s*(["'])\s*(?:javascript|data|vbscript):[^"']*\2/gi, '$1=$2#blocked$2');
36
- }
37
-
38
- function renderMd(md) {
39
- const clean = stripFrontmatter(md);
40
- if (typeof marked === 'undefined') {
41
- return '<pre>' + clean.replace(/</g, '&lt;') + '</pre>';
42
- }
43
- return sanitizeHtml(marked.parse(clean));
44
- }
15
+ import { FileReader } from '../components/FileReader.js';
45
16
 
46
17
  // ---- File tree components ----
47
18
  function FileEntry({ file, extraText, onSelect, isSelected }) {
@@ -117,51 +88,14 @@ function FileGroup({ group, onSelect, selectedPath, filter }) {
117
88
  return null;
118
89
  }
119
90
 
120
- // ---- File content pane ----
121
- function FileContent({ path, html: htmlContent, loading, error }) {
122
- if (loading) {
123
- return html`
124
- <div id="file-view">
125
- <div class="skeleton"></div>
126
- <div class="skeleton" style="height:200px;"></div>
127
- </div>
128
- `;
129
- }
130
- if (error) {
131
- return html`
132
- <div id="file-view">
133
- <div style="color:var(--accent-red);padding:16px;">${error}</div>
134
- </div>
135
- `;
136
- }
137
- if (!path || !htmlContent) return html`<div id="file-view"></div>`;
138
-
139
- function copyPath() {
140
- navigator.clipboard.writeText(path).then(() => {
141
- showToast('Path copied!');
142
- }).catch(() => {});
143
- }
144
-
145
- return html`
146
- <div id="file-view">
147
- <div class="file-path-header">
148
- <span>${path}</span>
149
- <button class="copy-btn" onClick=${copyPath}>Copy</button>
150
- </div>
151
- <div class="md-render" dangerouslySetInnerHTML=${{ __html: htmlContent }} />
152
- </div>
153
- `;
154
- }
155
-
156
91
  // ---- Root FilesView ----
157
92
  export function FilesView() {
158
93
  const { requestedFile } = useStore();
159
94
 
160
- const [groups, setGroups] = useState([]);
161
- const [loading, setLoading] = useState(true);
162
- const [filter, setFilter] = useState('');
163
- const [selectedPath, setSelectedPath] = useState(null);
164
- const [fileContent, setFileContent] = useState({ html: null, loading: false, error: null });
95
+ const [groups, setGroups] = useState([]);
96
+ const [loading, setLoading] = useState(true);
97
+ const [filter, setFilter] = useState('');
98
+ const [selected, setSelected] = useState(null); // { path, label } | null
165
99
 
166
100
  // Fetch file tree on mount
167
101
  useEffect(() => {
@@ -173,33 +107,15 @@ export function FilesView() {
173
107
  .finally(() => setLoading(false));
174
108
  }, []);
175
109
 
176
- // Agent-jump bridge: requestedFile set by AgentsView
110
+ // Agent-jump bridge: the agent drawer's "View file in Files" sets
111
+ // requestedFile to a project-relative path — open it in the reader.
177
112
  useEffect(() => {
178
113
  if (!requestedFile) return;
179
- setFilter(requestedFile);
114
+ setSelected({ path: requestedFile, label: requestedFile.split('/').pop() });
180
115
  // Clear the bridge field so this doesn't re-trigger
181
116
  setState({ requestedFile: null });
182
117
  }, [requestedFile]);
183
118
 
184
- const loadFile = useCallback(async (file) => {
185
- setSelectedPath(file.path);
186
- setFileContent({ html: null, loading: true, error: null });
187
- try {
188
- const resp = await fetch('/api/file?path=' + encodeURIComponent(file.path));
189
- if (!resp.ok) {
190
- const msg = resp.status === 404
191
- ? 'File not found: ' + file.path
192
- : 'Failed to load file (HTTP ' + resp.status + ').';
193
- setFileContent({ html: null, loading: false, error: msg });
194
- return;
195
- }
196
- const text = await resp.text();
197
- setFileContent({ html: renderMd(text), loading: false, error: null });
198
- } catch {
199
- setFileContent({ html: null, loading: false, error: 'Network error.' });
200
- }
201
- }, []);
202
-
203
119
  return html`
204
120
  <div class="view active" id="view-files">
205
121
  <div class="view-title">Files</div>
@@ -222,20 +138,21 @@ export function FilesView() {
222
138
  <${FileGroup}
223
139
  key=${g.group}
224
140
  group=${g}
225
- onSelect=${loadFile}
226
- selectedPath=${selectedPath}
141
+ onSelect=${setSelected}
142
+ selectedPath=${selected && selected.path}
227
143
  filter=${filter}
228
144
  />
229
145
  `)
230
146
  }
231
147
  </div>
232
148
  </div>
233
- <${FileContent}
234
- path=${selectedPath}
235
- html=${fileContent.html}
236
- loading=${fileContent.loading}
237
- error=${fileContent.error}
238
- />
149
+ ${selected && html`
150
+ <${FileReader}
151
+ path=${selected.path}
152
+ title=${selected.label}
153
+ onClose=${() => setSelected(null)}
154
+ />
155
+ `}
239
156
  </div>
240
157
  `;
241
158
  }
@@ -9,8 +9,9 @@
9
9
 
10
10
  import { html, useState, useCallback } from '../preact.js';
11
11
  import { useStore, refresh } from '../store.js';
12
- import { allTasks } from '../util.js';
13
- import { runStory, stopStory, openOrchPanel } from '../orchestrator.js';
12
+ import { allTasks, currentPhaseName } from '../util.js';
13
+ import { stopStory, openOrchPanel } from '../orchestrator.js';
14
+ import { openRunnerPicker } from '../components/RunnerPicker.js';
14
15
  import { showToast } from '../components/shared.js';
15
16
 
16
17
  // ---- Column descriptors ----
@@ -30,26 +31,26 @@ function kanbanCol(status) {
30
31
  }
31
32
 
32
33
  /** Return the effective column, hoisting to in_progress if a live session exists. */
33
- function effCol(task, activeSessions) {
34
- const running = Array.isArray(activeSessions)
35
- ? activeSessions.some(s => s.storyId === task.id && s.status === 'running')
36
- : false;
37
- return (task.id && running) ? 'in_progress' : kanbanCol(task.status);
34
+ function effCol(task, runningByStory) {
35
+ const running = !!(task.id && runningByStory && runningByStory[task.id]);
36
+ return running ? 'in_progress' : kanbanCol(task.status);
38
37
  }
39
38
 
40
39
  // ---- Card component ----
41
- function KanbanCard({ task, col, onDragStart, onDragEnd }) {
40
+ function KanbanCard({ task, col, live, orchDown, onDragStart, onDragEnd }) {
42
41
  const sid = task.id || '';
43
42
  const c = col;
44
- const isRunning = c === 'in_progress';
45
- const canRun = c === 'todo' || c === 'blocked';
43
+ const isRunning = !!live; // live orchestrator session, not just status
44
+ const canRun = !isRunning && (c === 'todo' || c === 'blocked');
46
45
  const pts = task.points ? task.points + 'p' : null;
47
46
  const phase = task.phaseId ? 'P' + task.phaseId : null;
48
47
  const sprintMeta = [pts, phase].filter(Boolean).join(' · ');
49
48
 
50
49
  function handleRun(e) {
51
50
  e.stopPropagation();
52
- runStory(sid);
51
+ openRunnerPicker(e.currentTarget, {
52
+ kind: 'session', storyId: sid, cmd: '/rcode-dev-story ' + sid, title: sid,
53
+ });
53
54
  }
54
55
  function handleStop(e) {
55
56
  e.stopPropagation();
@@ -80,13 +81,15 @@ function KanbanCard({ task, col, onDragStart, onDragEnd }) {
80
81
  ` : null}
81
82
  ${isRunning ? html`
82
83
  <div class="card-run-indicator" id=${'run-ind-' + sid}>
83
- <span class="run-pulse"></span>running
84
+ <span class="live-dot"></span>running
84
85
  </div>
85
86
  ` : null}
86
87
  ${sid ? html`
87
88
  <div class="kanban-card-actions">
88
89
  ${canRun ? html`
89
- <button class="kanban-run-btn" onClick=${handleRun}>▶ Run</button>
90
+ <button class="kanban-run-btn" disabled=${orchDown}
91
+ title=${orchDown ? 'Orchestrator unreachable' : 'Run ' + sid}
92
+ onClick=${handleRun}>▶ Run</button>
90
93
  ` : isRunning ? html`
91
94
  <button class="kanban-stop-btn" onClick=${handleStop}>■ Stop</button>
92
95
  <button class="kanban-view-btn" onClick=${handleView}>↗ View</button>
@@ -99,8 +102,19 @@ function KanbanCard({ task, col, onDragStart, onDragEnd }) {
99
102
  `;
100
103
  }
101
104
 
105
+ /**
106
+ * Orchestrator status dot — reflects the 4s session-poll reachability.
107
+ * up (green pulse) / down (red) / neutral until the first poll lands.
108
+ */
109
+ function OrchDot({ online }) {
110
+ const cls = 'orch-status-dot' + (online === false ? ' down' : online ? ' up' : '');
111
+ const label = online === false ? 'Orchestrator unreachable'
112
+ : online ? 'Orchestrator online' : 'Orchestrator status unknown';
113
+ return html`<span class=${cls} id="orch-dot" title=${label} role="img" aria-label=${label}></span>`;
114
+ }
115
+
102
116
  // ---- Column component ----
103
- function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }) {
117
+ function KanbanColumn({ col, cards, runningByStory, orchDown, onDragStart, onDragEnd, onDragOver, onDrop }) {
104
118
  return html`
105
119
  <div class=${'kanban-col ' + col.cssClass} data-col=${col.id}>
106
120
  <div class="kanban-col-head">
@@ -120,6 +134,8 @@ function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }
120
134
  key=${t.id || t.title}
121
135
  task=${t}
122
136
  col=${col.id}
137
+ live=${!!(t.id && runningByStory && runningByStory[t.id])}
138
+ orchDown=${orchDown}
123
139
  onDragStart=${e => onDragStart(e, t)}
124
140
  onDragEnd=${onDragEnd}
125
141
  />
@@ -131,8 +147,9 @@ function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }
131
147
 
132
148
  // ---- Root KanbanView ----
133
149
  export function KanbanView() {
134
- const { phases, activeSessions, currentPhase, milestone } = useStore();
150
+ const { phases, runningByStory, currentPhase, milestone, orchOnline } = useStore();
135
151
  const tasks = allTasks(phases);
152
+ const orchDown = orchOnline === false;
136
153
 
137
154
  // ---- Local column state (visual DnD overrides) ----
138
155
  // Map<taskId, colId> — overrides the store-derived column for visual-only moves.
@@ -141,7 +158,7 @@ export function KanbanView() {
141
158
 
142
159
  function getColFor(task) {
143
160
  if (visualMoves[task.id]) return visualMoves[task.id];
144
- return effCol(task, activeSessions);
161
+ return effCol(task, runningByStory);
145
162
  }
146
163
 
147
164
  // Build buckets
@@ -190,7 +207,7 @@ export function KanbanView() {
190
207
  <div class="view active" id="view-kanban">
191
208
  <div class="kanban-topbar">
192
209
  <div class="kanban-topbar-title">
193
- <span class="orch-status-dot" id="orch-dot"></span>
210
+ <${OrchDot} online=${orchOnline} />
194
211
  Kanban
195
212
  </div>
196
213
  <div class="kanban-topbar-actions">
@@ -200,11 +217,11 @@ export function KanbanView() {
200
217
  </div>
201
218
  <div class="empty" style="margin:24px;">
202
219
  No stories yet.
203
- ${(milestone || currentPhase) ? html`
220
+ ${(milestone || currentPhaseName(currentPhase)) ? html`
204
221
  <div class="empty-action">
205
222
  ${milestone ? html`Milestone <strong>${milestone}</strong>` : null}
206
- ${milestone && currentPhase ? ' · ' : null}
207
- ${currentPhase ? html`Phase <strong>${currentPhase}</strong>` : null}
223
+ ${milestone && currentPhaseName(currentPhase) ? ' · ' : null}
224
+ ${currentPhaseName(currentPhase) ? html`Phase <strong>${currentPhaseName(currentPhase)}</strong>` : null}
208
225
  ${' is active.'}
209
226
  </div>
210
227
  ` : null}
@@ -221,7 +238,7 @@ export function KanbanView() {
221
238
  <div class="view active" id="view-kanban">
222
239
  <div class="kanban-topbar">
223
240
  <div class="kanban-topbar-title">
224
- <span class="orch-status-dot" id="orch-dot"></span>
241
+ <${OrchDot} online=${orchOnline} />
225
242
  Kanban
226
243
  </div>
227
244
  <div class="kanban-topbar-actions">
@@ -235,6 +252,8 @@ export function KanbanView() {
235
252
  key=${col.id}
236
253
  col=${col}
237
254
  cards=${buckets[col.id] || []}
255
+ runningByStory=${runningByStory}
256
+ orchDown=${orchDown}
238
257
  onDragStart=${handleDragStart}
239
258
  onDragEnd=${handleDragEnd}
240
259
  onDragOver=${handleDragOver}
@@ -7,9 +7,13 @@
7
7
  * populated — sections map + distillates / change records / archive / post-mortems
8
8
  *
9
9
  * Command hints accordion mirrors the legacy cmdAccordion() output.
10
+ *
11
+ * Clicking an existing entry opens its content in the shared FileReader
12
+ * slide-over (same component the Files view uses).
10
13
  */
11
14
 
12
15
  import { html, useState, useEffect } from '../preact.js';
16
+ import { FileReader } from '../components/FileReader.js';
13
17
 
14
18
  // ---- Command hints accordion ----
15
19
  const MEMORY_HINTS = [
@@ -39,7 +43,7 @@ function CmdAccordion({ hints }) {
39
43
  }
40
44
 
41
45
  // ---- Section file list ----
42
- function SectionGroup({ section, files }) {
46
+ function SectionGroup({ section, files, onOpen }) {
43
47
  return html`
44
48
  <div>
45
49
  <div class="memory-group-header">${section}</div>
@@ -47,8 +51,13 @@ function SectionGroup({ section, files }) {
47
51
  ${files.map(f => {
48
52
  const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
49
53
  const meta = f.exists ? (f.populated ? 'populated' : 'template only') : 'missing';
54
+ // Only existing files are openable — missing entries stay inert.
50
55
  return html`
51
- <div class="item" key=${f.name}>
56
+ <div
57
+ class=${'item' + (f.exists ? ' item-clickable' : '')}
58
+ key=${f.name}
59
+ onClick=${f.exists ? () => onOpen(f) : undefined}
60
+ >
52
61
  <div class="item-title">${status} ${f.name}</div>
53
62
  <div class="item-meta">${meta} · ${f.bytes || 0} bytes</div>
54
63
  </div>
@@ -60,14 +69,14 @@ function SectionGroup({ section, files }) {
60
69
  }
61
70
 
62
71
  // ---- Generic list group (distillates, change records, etc.) ----
63
- function ListGroup({ label, items }) {
72
+ function ListGroup({ label, items, onOpen }) {
64
73
  if (!items || !items.length) return null;
65
74
  return html`
66
75
  <div>
67
76
  <div class="memory-group-header">${label} (${items.length})</div>
68
77
  <div class="decision-list">
69
78
  ${items.map(f => html`
70
- <div class="item" key=${f.name}>
79
+ <div class="item item-clickable" key=${f.name} onClick=${() => onOpen(f)}>
71
80
  <div class="item-title">${f.name}</div>
72
81
  </div>
73
82
  `)}
@@ -81,6 +90,7 @@ export function MemoryView() {
81
90
  const [memory, setMemory] = useState(null);
82
91
  const [loading, setLoading] = useState(true);
83
92
  const [error, setError] = useState(null);
93
+ const [reader, setReader] = useState(null); // { path, name } | null
84
94
 
85
95
  useEffect(() => {
86
96
  setLoading(true);
@@ -144,14 +154,21 @@ export function MemoryView() {
144
154
  </div>
145
155
  <div id="memory-sections">
146
156
  ${Object.entries(sections).map(([section, files]) => html`
147
- <${SectionGroup} key=${section} section=${section} files=${files} />
157
+ <${SectionGroup} key=${section} section=${section} files=${files} onOpen=${setReader} />
148
158
  `)}
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} />
159
+ <${ListGroup} label="Distillates" items=${memory.distillates} onOpen=${setReader} />
160
+ <${ListGroup} label="Change Records" items=${memory.changeRecords} onOpen=${setReader} />
161
+ <${ListGroup} label="Milestone Archive" items=${memory.archive} onOpen=${setReader} />
162
+ <${ListGroup} label="Post-mortems" items=${memory.postMortems} onOpen=${setReader} />
153
163
  </div>
154
164
  <${CmdAccordion} hints=${MEMORY_HINTS} />
165
+ ${reader && html`
166
+ <${FileReader}
167
+ path=${reader.path}
168
+ title=${reader.name}
169
+ onClose=${() => setReader(null)}
170
+ />
171
+ `}
155
172
  </div>
156
173
  `;
157
174
  }
@@ -9,8 +9,8 @@
9
9
 
10
10
  import { html } from '../preact.js';
11
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';
12
+ import { pct, humanDate, allSprints, allTasks, currentPhaseName } from '../util.js';
13
+ import { CompletionRing, Breadcrumb, Tag, PhaseCard, pressable } from '../components/shared.js';
14
14
  import { runningTotal } from '../orchestrator.js';
15
15
  import { Icon } from '../icons-client.js';
16
16
 
@@ -94,7 +94,7 @@ export function MilestonesView({ subId }) {
94
94
  <div class="attr-grid">
95
95
  <${AttrItem} label="Total Phases" value=${phases.length}/>
96
96
  <${AttrItem} label="Completed Phases" value=${doneP}/>
97
- <${AttrItem} label="Current Phase" value=${S.currentPhase || '—'}/>
97
+ <${AttrItem} label="Current Phase" value=${currentPhaseName(S.currentPhase) || '—'}/>
98
98
  <${AttrItem} label="Current Sprint" value=${S.currentSprint || '—'}/>
99
99
  <${AttrItem} label="Tasks Done" value=${done.length + '/' + total.length}/>
100
100
  <${AttrItem} label="Progress" value=${pct(done.length, total.length)}/>
@@ -116,7 +116,7 @@ export function MilestonesView({ subId }) {
116
116
  <div id="view-milestones" class="view active">
117
117
  <div class="view-title">Milestones</div>
118
118
  <div class="phase-list">
119
- <div class="item item-clickable" onClick=${() => { location.hash = 'milestones/M1'; }}>
119
+ <div class="item item-clickable" ...${pressable(() => { location.hash = 'milestones/M1'; })}>
120
120
  <div style="display:flex;align-items:center;gap:var(--space-4);">
121
121
  <${CompletionRing} done=${done.length} total=${total.length}/>
122
122
  <div>