@hanzlaa/rcode 3.6.15 → 3.6.16
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/package.json +1 -1
- package/server/lib/html/client/components/App.js +56 -19
- package/server/lib/html/client/components/Sidebar.js +13 -0
- package/server/lib/html/client/components/Topbar.js +6 -3
- package/server/lib/html/client/store.js +30 -0
- package/server/lib/html/client/util.js +0 -10
- package/server/lib/html/client/views/FilesView.js +4 -1
- package/server/lib/html/client/views/KanbanView.js +15 -4
- package/server/lib/html/client/views/OrchestrationView.js +12 -2
- package/server/lib/html/client/views/OverviewView.js +44 -4
- package/server/lib/html/client.js +7 -0
- package/server/lib/html/css.js +16 -0
- package/server/lib/scanner.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.16",
|
|
4
4
|
"description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { html, useState, useEffect, useRef, useCallback } from '../preact.js';
|
|
16
|
-
import { getState, setState, subscribe } from '../store.js';
|
|
16
|
+
import { getState, setState, subscribe, registerRefresh } from '../store.js';
|
|
17
17
|
import { startSessionsPoll, refreshOrchToken } from '../orchestrator.js';
|
|
18
18
|
import { Sidebar } from './Sidebar.js';
|
|
19
19
|
import { Topbar } from './Topbar.js';
|
|
@@ -65,6 +65,35 @@ function parseHash() {
|
|
|
65
65
|
return { view: resolvedView, subId };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Full-width banner shown when /api/state polling is failing. */
|
|
69
|
+
function OfflineBanner({ offline }) {
|
|
70
|
+
if (!offline) return null;
|
|
71
|
+
const s = 'display:flex;align-items:center;gap:var(--space-2);'
|
|
72
|
+
+ 'padding:var(--space-2) var(--space-4);background:var(--red,#eb5757);'
|
|
73
|
+
+ 'color:#fff;font-size:var(--text-sm);font-weight:600;';
|
|
74
|
+
return html`<div style=${s}>⚠ Dashboard offline — retrying every 30s…</div>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Thin IDE-style status bar: project path · rcode version · last refresh. */
|
|
78
|
+
function StatusBar({ projectRoot, projectName, version, updatedAgo, offline, refreshing }) {
|
|
79
|
+
const bar = 'display:flex;align-items:center;gap:var(--space-4);height:24px;'
|
|
80
|
+
+ 'padding:0 var(--space-4);background:var(--bg-elev-1);'
|
|
81
|
+
+ 'border-top:1px solid var(--border-subtle);font-family:var(--font-mono);'
|
|
82
|
+
+ 'font-size:var(--text-2xs);color:var(--text-muted);white-space:nowrap;overflow:hidden;';
|
|
83
|
+
const dot = 'width:6px;height:6px;border-radius:50%;flex-shrink:0;background:'
|
|
84
|
+
+ (offline ? 'var(--red,#eb5757)' : 'var(--accent-green)') + ';'
|
|
85
|
+
+ (refreshing ? 'animation:pulse-dot 1s ease-in-out infinite;' : '');
|
|
86
|
+
const path = projectRoot || projectName || 'no project';
|
|
87
|
+
return html`
|
|
88
|
+
<footer style=${bar}>
|
|
89
|
+
<span style=${dot}></span>
|
|
90
|
+
<span style="overflow:hidden;text-overflow:ellipsis;" title=${path}>${path}</span>
|
|
91
|
+
<span style="margin-left:auto;">rcode v${version || '?'}</span>
|
|
92
|
+
<span>${offline ? 'offline' : refreshing ? 'syncing…' : 'updated ' + updatedAgo}</span>
|
|
93
|
+
</footer>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/** Root App component. No props needed — reads everything from the store. */
|
|
69
98
|
export function App() {
|
|
70
99
|
// ---- Router state ----
|
|
@@ -121,21 +150,21 @@ export function App() {
|
|
|
121
150
|
return () => clearInterval(id);
|
|
122
151
|
}, []);
|
|
123
152
|
|
|
124
|
-
// ----
|
|
153
|
+
// ---- Refresh (manual + 30s poll share this) ----
|
|
125
154
|
const lastScannedRef = useRef(null);
|
|
126
155
|
|
|
127
156
|
const fetchAndRerender = useCallback(async () => {
|
|
128
|
-
|
|
129
|
-
if (btn) btn.textContent = '↺ …';
|
|
157
|
+
setState({ refreshing: true });
|
|
130
158
|
try {
|
|
131
159
|
const r = await fetch('/api/state');
|
|
132
|
-
if (!r.ok) return;
|
|
160
|
+
if (!r.ok) { setState({ refreshing: false, offline: true }); return; }
|
|
133
161
|
const newState = await r.json();
|
|
134
162
|
lastScannedRef.current = newState.lastScanned;
|
|
135
163
|
scanTimeRef.current = Date.now();
|
|
136
164
|
setUpdatedAgo('just now');
|
|
165
|
+
const patch = { refreshing: false, offline: false, lastRefresh: Date.now() };
|
|
137
166
|
if (newState.raw) {
|
|
138
|
-
|
|
167
|
+
Object.assign(patch, {
|
|
139
168
|
phases: newState.phaseTree || newState.raw.phases || [],
|
|
140
169
|
milestone: newState.raw.milestone || '',
|
|
141
170
|
currentPhase: newState.raw.current_phase || null,
|
|
@@ -146,26 +175,23 @@ export function App() {
|
|
|
146
175
|
last_session: newState.raw.last_session || null,
|
|
147
176
|
});
|
|
148
177
|
}
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
setState(patch);
|
|
179
|
+
} catch {
|
|
180
|
+
// Network failure → mark offline so the banner shows; the poll keeps retrying.
|
|
181
|
+
setState({ refreshing: false, offline: true });
|
|
182
|
+
}
|
|
151
183
|
}, []);
|
|
152
184
|
|
|
153
185
|
// ---- 30s auto-refresh ----
|
|
154
186
|
useEffect(() => {
|
|
155
|
-
const id = setInterval(
|
|
156
|
-
try {
|
|
157
|
-
const r = await fetch('/api/state');
|
|
158
|
-
if (!r.ok) return;
|
|
159
|
-
const s = await r.json();
|
|
160
|
-
if (s && s.lastScanned !== lastScannedRef.current) await fetchAndRerender();
|
|
161
|
-
} catch { /* ignore */ }
|
|
162
|
-
}, 30000);
|
|
187
|
+
const id = setInterval(fetchAndRerender, 30000);
|
|
163
188
|
return () => clearInterval(id);
|
|
164
189
|
}, [fetchAndRerender]);
|
|
165
190
|
|
|
166
|
-
//
|
|
191
|
+
// Register the refresh handler with the store so any component can call
|
|
192
|
+
// refresh() directly. Also keeps window._preactRefresh in sync for legacy.
|
|
167
193
|
useEffect(() => {
|
|
168
|
-
|
|
194
|
+
registerRefresh(fetchAndRerender);
|
|
169
195
|
}, [fetchAndRerender]);
|
|
170
196
|
|
|
171
197
|
// Start the global session poll and refresh the orchestrator token on boot.
|
|
@@ -189,10 +215,11 @@ export function App() {
|
|
|
189
215
|
document.body.classList.remove('sidebar-visible');
|
|
190
216
|
}}></div>
|
|
191
217
|
|
|
192
|
-
<div class="content-area" id="main-content">
|
|
218
|
+
<div class="content-area" id="main-content" style="grid-template-rows:44px 1fr auto;">
|
|
193
219
|
<${Topbar}
|
|
194
220
|
projectName=${storeState.projectName || ''}
|
|
195
221
|
updatedAgo=${updatedAgo}
|
|
222
|
+
refreshing=${storeState.refreshing}
|
|
196
223
|
onRefresh=${fetchAndRerender}
|
|
197
224
|
onToggleTheme=${toggleTheme}
|
|
198
225
|
onToggleSidebar=${toggleSidebar}
|
|
@@ -200,8 +227,18 @@ export function App() {
|
|
|
200
227
|
/>
|
|
201
228
|
|
|
202
229
|
<div class="main-scroll" id="main-scroll">
|
|
230
|
+
<${OfflineBanner} offline=${storeState.offline} />
|
|
203
231
|
${PreactView ? html`<${PreactView} subId=${subId} />` : null}
|
|
204
232
|
</div>
|
|
233
|
+
|
|
234
|
+
<${StatusBar}
|
|
235
|
+
projectRoot=${storeState.projectRoot}
|
|
236
|
+
projectName=${storeState.projectName}
|
|
237
|
+
version=${storeState.version}
|
|
238
|
+
updatedAgo=${updatedAgo}
|
|
239
|
+
offline=${storeState.offline}
|
|
240
|
+
refreshing=${storeState.refreshing}
|
|
241
|
+
/>
|
|
205
242
|
</div>
|
|
206
243
|
|
|
207
244
|
<${XtermPanel} />
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { html } from '../preact.js';
|
|
9
9
|
import { Icon } from '../icons-client.js';
|
|
10
|
+
import { useStore } from '../store.js';
|
|
11
|
+
import { allSprints, allTasks } from '../util.js';
|
|
12
|
+
import { AGENTS } from '../agents-data.js';
|
|
10
13
|
|
|
11
14
|
// Nav structure: [ { section, links: [ { view, icon, label } ] } ]
|
|
12
15
|
const NAV_SECTIONS = [
|
|
@@ -47,6 +50,15 @@ const NAV_SECTIONS = [
|
|
|
47
50
|
* projectName {string} — displayed under the "Rihal" label
|
|
48
51
|
*/
|
|
49
52
|
export function Sidebar({ activeView, projectName }) {
|
|
53
|
+
const S = useStore();
|
|
54
|
+
const counts = {
|
|
55
|
+
phases: (S.phases || []).length,
|
|
56
|
+
sprints: allSprints(S.phases).length,
|
|
57
|
+
tasks: allTasks(S.phases).length,
|
|
58
|
+
decisions: (S.decisions || []).length,
|
|
59
|
+
agents: AGENTS.length,
|
|
60
|
+
};
|
|
61
|
+
|
|
50
62
|
return html`
|
|
51
63
|
<aside class="sidebar" id="sidebar">
|
|
52
64
|
<div class="sidebar-project">
|
|
@@ -64,6 +76,7 @@ export function Sidebar({ activeView, projectName }) {
|
|
|
64
76
|
>
|
|
65
77
|
<${Icon} name=${icon} size=${14} />
|
|
66
78
|
${' ' + label}
|
|
79
|
+
${counts[view] ? html`<span class="nav-count">${counts[view]}</span>` : null}
|
|
67
80
|
</button>
|
|
68
81
|
`)}
|
|
69
82
|
`)}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { html } from '../preact.js';
|
|
16
16
|
import { Icon } from '../icons-client.js';
|
|
17
17
|
|
|
18
|
-
export function Topbar({ projectName, updatedAgo, onRefresh, onToggleTheme, onToggleSidebar, themeLabel }) {
|
|
18
|
+
export function Topbar({ projectName, updatedAgo, refreshing, onRefresh, onToggleTheme, onToggleSidebar, themeLabel }) {
|
|
19
19
|
return html`
|
|
20
20
|
<header>
|
|
21
21
|
<div class="topbar-start-group">
|
|
@@ -36,8 +36,11 @@ export function Topbar({ projectName, updatedAgo, onRefresh, onToggleTheme, onTo
|
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
38
38
|
<div class="header-actions">
|
|
39
|
-
<span class="live" id="live-dot" title="Live"
|
|
40
|
-
|
|
39
|
+
<span class="live" id="live-dot" title="Live"
|
|
40
|
+
style=${refreshing ? 'animation-duration:0.7s;background:var(--accent-blue);' : ''}></span>
|
|
41
|
+
<span id="updated-ago" class="updated-ago">
|
|
42
|
+
${refreshing ? '⟳ syncing…' : (updatedAgo || 'just now')}
|
|
43
|
+
</span>
|
|
41
44
|
<button class="header-btn" id="refresh-btn" onClick=${onRefresh}>↺ Refresh</button>
|
|
42
45
|
<!-- icon shows TARGET state (not current): dark→sun means "click to go light"; light→moon means "click to go dark" -->
|
|
43
46
|
<button class="header-btn" id="theme-btn" onClick=${onToggleTheme} title="Toggle theme"><${Icon} name=${themeLabel === 'light' ? 'moon' : 'sun'} size=${14}/></button>
|
|
@@ -27,6 +27,16 @@ let _state = {
|
|
|
27
27
|
workstreams: _seed.workstreams || [],
|
|
28
28
|
pendingHandoff: _seed.pendingHandoff || null,
|
|
29
29
|
memoryBank: _seed.memoryBank || null,
|
|
30
|
+
// Environment info — surfaced in the bottom status bar.
|
|
31
|
+
projectName: _seed.projectName || '',
|
|
32
|
+
projectRoot: _seed.projectRoot || '',
|
|
33
|
+
version: _seed.version || '',
|
|
34
|
+
// Refresh lifecycle: refreshing flips true during a poll/fetch; offline is
|
|
35
|
+
// true when /api/state fails; lastRefresh is the ms timestamp of the last
|
|
36
|
+
// successful fetch (null until the first one completes).
|
|
37
|
+
refreshing: false,
|
|
38
|
+
offline: false,
|
|
39
|
+
lastRefresh: null,
|
|
30
40
|
// Live orchestrator sessions (populated by startSessionsPoll in orchestrator.js)
|
|
31
41
|
activeSessions: [],
|
|
32
42
|
// File jump bridge: AgentsView sets this to a slug so FilesView opens it.
|
|
@@ -75,6 +85,26 @@ export function subscribe(fn) {
|
|
|
75
85
|
return () => _subscribers.delete(fn);
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Refresh handler bridge.
|
|
90
|
+
*
|
|
91
|
+
* App owns the actual /api/state fetch (fetchAndRerender). It registers that
|
|
92
|
+
* function here on mount so any component can trigger a refresh via refresh()
|
|
93
|
+
* without reaching for a window global. `window._preactRefresh` is kept in
|
|
94
|
+
* sync for any legacy inline-onclick callers.
|
|
95
|
+
*/
|
|
96
|
+
let _refreshHandler = null;
|
|
97
|
+
|
|
98
|
+
export function registerRefresh(fn) {
|
|
99
|
+
_refreshHandler = typeof fn === 'function' ? fn : null;
|
|
100
|
+
if (typeof window !== 'undefined') window._preactRefresh = _refreshHandler;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Trigger a data refresh, if a handler has been registered. */
|
|
104
|
+
export function refresh() {
|
|
105
|
+
if (typeof _refreshHandler === 'function') return _refreshHandler();
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
/**
|
|
79
109
|
* Preact hook. Subscribes the calling component to the store and
|
|
80
110
|
* returns the current state. The component re-renders on every setState().
|
|
@@ -7,16 +7,6 @@
|
|
|
7
7
|
* Import here; do NOT duplicate in component files.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
/** HTML-escape a value for safe rendering. */
|
|
11
|
-
export function esc(s) {
|
|
12
|
-
return String(s || '')
|
|
13
|
-
.replace(/&/g, '&')
|
|
14
|
-
.replace(/</g, '<')
|
|
15
|
-
.replace(/>/g, '>')
|
|
16
|
-
.replace(/"/g, '"')
|
|
17
|
-
.replace(/'/g, ''');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
10
|
/** Percentage string. Returns '—' if total is 0. */
|
|
21
11
|
export function pct(done, total) {
|
|
22
12
|
return total > 0 ? Math.round(done / total * 100) + '%' : '—';
|
|
@@ -172,7 +172,10 @@ export function FilesView() {
|
|
|
172
172
|
try {
|
|
173
173
|
const resp = await fetch('/api/file?path=' + encodeURIComponent(file.path));
|
|
174
174
|
if (!resp.ok) {
|
|
175
|
-
|
|
175
|
+
const msg = resp.status === 404
|
|
176
|
+
? 'File not found: ' + file.path
|
|
177
|
+
: 'Failed to load file (HTTP ' + resp.status + ').';
|
|
178
|
+
setFileContent({ html: null, loading: false, error: msg });
|
|
176
179
|
return;
|
|
177
180
|
}
|
|
178
181
|
const text = await resp.text();
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { html, useState, useCallback } from '../preact.js';
|
|
11
|
-
import { useStore } from '../store.js';
|
|
11
|
+
import { useStore, refresh } from '../store.js';
|
|
12
12
|
import { allTasks } from '../util.js';
|
|
13
13
|
import { runStory, stopStory, openOrchPanel } from '../orchestrator.js';
|
|
14
14
|
import { showToast } from '../components/shared.js';
|
|
@@ -131,7 +131,7 @@ function KanbanColumn({ col, cards, onDragStart, onDragEnd, onDragOver, onDrop }
|
|
|
131
131
|
|
|
132
132
|
// ---- Root KanbanView ----
|
|
133
133
|
export function KanbanView() {
|
|
134
|
-
const { phases, activeSessions } = useStore();
|
|
134
|
+
const { phases, activeSessions, currentPhase, milestone } = useStore();
|
|
135
135
|
const tasks = allTasks(phases);
|
|
136
136
|
|
|
137
137
|
// ---- Local column state (visual DnD overrides) ----
|
|
@@ -178,7 +178,7 @@ export function KanbanView() {
|
|
|
178
178
|
|
|
179
179
|
// ---- Manual refresh ----
|
|
180
180
|
function handleSync() {
|
|
181
|
-
|
|
181
|
+
refresh();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function handleSessions() {
|
|
@@ -200,7 +200,18 @@ export function KanbanView() {
|
|
|
200
200
|
</div>
|
|
201
201
|
<div class="empty" style="margin:24px;">
|
|
202
202
|
No stories yet.
|
|
203
|
-
|
|
203
|
+
${(milestone || currentPhase) ? html`
|
|
204
|
+
<div class="empty-action">
|
|
205
|
+
${milestone ? html`Milestone <strong>${milestone}</strong>` : null}
|
|
206
|
+
${milestone && currentPhase ? ' · ' : null}
|
|
207
|
+
${currentPhase ? html`Phase <strong>${currentPhase}</strong>` : null}
|
|
208
|
+
${' is active.'}
|
|
209
|
+
</div>
|
|
210
|
+
` : null}
|
|
211
|
+
<div class="empty-action">
|
|
212
|
+
Run <code>/rihal-plan</code> to generate sprint stories, or browse
|
|
213
|
+
planning docs in the <a href="#files">Files</a> view.
|
|
214
|
+
</div>
|
|
204
215
|
</div>
|
|
205
216
|
</div>
|
|
206
217
|
`;
|
|
@@ -131,6 +131,13 @@ function CommandRunner() {
|
|
|
131
131
|
: html`<${Icon} name="play" size=${14}/> Run`}
|
|
132
132
|
</button>
|
|
133
133
|
</div>
|
|
134
|
+
<div class="cmd-runner-hint">
|
|
135
|
+
${isRunning
|
|
136
|
+
? html`Command is running — output 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.`}
|
|
140
|
+
</div>
|
|
134
141
|
</div>
|
|
135
142
|
`;
|
|
136
143
|
}
|
|
@@ -152,8 +159,11 @@ export function OrchestrationView() {
|
|
|
152
159
|
|
|
153
160
|
${sessions.length === 0 ? html`
|
|
154
161
|
<div class="empty">
|
|
155
|
-
No
|
|
156
|
-
<div class="empty-action">
|
|
162
|
+
No active execution.
|
|
163
|
+
<div class="empty-action">
|
|
164
|
+
Use the Command Runner above, or run <code>/rihal-execute</code> to
|
|
165
|
+
start a phase or sprint.
|
|
166
|
+
</div>
|
|
157
167
|
</div>
|
|
158
168
|
` : html`
|
|
159
169
|
<div class="orch-grid">
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { html } from '../preact.js';
|
|
9
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';
|
|
10
|
+
import { pct, humanDate, allSprints, allTasks, chip, sprintHints as getSprintHints } from '../util.js';
|
|
11
|
+
import { ProgressBar, CmdHints, Chip } from '../components/shared.js';
|
|
12
12
|
import { Icon } from '../icons-client.js';
|
|
13
13
|
|
|
14
14
|
// ---- OverviewView ----
|
|
@@ -38,6 +38,41 @@ export function OverviewView() {
|
|
|
38
38
|
hints = [['/rihal-resume-work','Resume from the pending handoff'], ...hints];
|
|
39
39
|
}
|
|
40
40
|
|
|
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;
|
|
50
|
+
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'}
|
|
56
|
+
</div>
|
|
57
|
+
</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
|
+
|
|
41
76
|
// Current sprint progress
|
|
42
77
|
function SprintProgress() {
|
|
43
78
|
if (!curSprint) return null;
|
|
@@ -175,8 +210,12 @@ export function OverviewView() {
|
|
|
175
210
|
if (!S.memoryBank || !S.memoryBank.active) return null;
|
|
176
211
|
const m = S.memoryBank.active;
|
|
177
212
|
return html`
|
|
178
|
-
<section
|
|
179
|
-
|
|
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>
|
|
180
219
|
<div class="body">
|
|
181
220
|
<div class="attr-grid">
|
|
182
221
|
<div class="attr-item">
|
|
@@ -207,6 +246,7 @@ export function OverviewView() {
|
|
|
207
246
|
return html`
|
|
208
247
|
<div id="view-overview" class="view active">
|
|
209
248
|
<div class="stats">
|
|
249
|
+
<${StatusSummary}/>
|
|
210
250
|
<${VelocitySpark}/>
|
|
211
251
|
</div>
|
|
212
252
|
<${HandoffBanner}/>
|
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
const { ICONS } = require('./icons');
|
|
16
16
|
|
|
17
|
+
// rcode version — read once at module load; surfaced in the dashboard status bar.
|
|
18
|
+
let RCODE_VERSION = '';
|
|
19
|
+
try { RCODE_VERSION = require('../../../package.json').version || ''; } catch { /* version unknown */ }
|
|
20
|
+
|
|
17
21
|
// Fields the client needs from the scanned state. Kept in sync with
|
|
18
22
|
// store.js initial state and the view components that read it.
|
|
19
23
|
function clientState(state) {
|
|
20
24
|
return JSON.stringify({
|
|
21
25
|
phases: state.phaseTree || state.raw?.phases || [],
|
|
26
|
+
projectName: state.projectName || '',
|
|
27
|
+
projectRoot: state.projectRoot || '',
|
|
28
|
+
version: RCODE_VERSION,
|
|
22
29
|
milestone: state.raw?.milestone || '',
|
|
23
30
|
currentPhase: state.raw?.current_phase || null,
|
|
24
31
|
currentSprint: state.raw?.current_sprint || null,
|
package/server/lib/html/css.js
CHANGED
|
@@ -226,6 +226,16 @@ html, body {
|
|
|
226
226
|
}
|
|
227
227
|
.nav-link:hover { background: var(--bg-hover); color: var(--text-secondary); }
|
|
228
228
|
.nav-link.active { background: var(--bg-elev-2); color: var(--text-primary); }
|
|
229
|
+
.nav-count {
|
|
230
|
+
margin-left: auto;
|
|
231
|
+
font-size: 10px;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
color: var(--text-muted);
|
|
234
|
+
background: var(--bg-elev-2);
|
|
235
|
+
padding: 1px 6px;
|
|
236
|
+
border-radius: var(--radius-2);
|
|
237
|
+
}
|
|
238
|
+
.nav-link.active .nav-count { color: var(--text-secondary); background: var(--bg-hover); }
|
|
229
239
|
|
|
230
240
|
/* Mobile hamburger */
|
|
231
241
|
.hamburger-btn {
|
|
@@ -2230,6 +2240,12 @@ footer {
|
|
|
2230
2240
|
gap: var(--space-3);
|
|
2231
2241
|
align-items: center;
|
|
2232
2242
|
}
|
|
2243
|
+
.cmd-runner-hint {
|
|
2244
|
+
margin-top: var(--space-3);
|
|
2245
|
+
font-size: var(--text-xs);
|
|
2246
|
+
color: var(--text-muted);
|
|
2247
|
+
line-height: 1.5;
|
|
2248
|
+
}
|
|
2233
2249
|
.cmd-runner-select {
|
|
2234
2250
|
flex: 1;
|
|
2235
2251
|
background: var(--bg-input, var(--bg-elev-2));
|
package/server/lib/scanner.js
CHANGED
|
@@ -85,11 +85,14 @@ function buildPhaseTree(projectDir, rawPhases) {
|
|
|
85
85
|
while ((tm = taskRe.exec(text))) {
|
|
86
86
|
const idM = tm[1].match(/id="([^"]+)"/);
|
|
87
87
|
const titleM = tm[2].match(/<title>([\s\S]*?)<\/title>/);
|
|
88
|
-
|
|
88
|
+
const acM = tm[2].match(/<acceptance_criteria>\s*([\s\S]*?)\s*<\/acceptance_criteria>/);
|
|
89
|
+
const story = {
|
|
89
90
|
id: idM ? idM[1] : `${sid}-task-${stories.length + 1}`,
|
|
90
91
|
title: titleM ? titleM[1].trim() : `Task ${stories.length + 1}`,
|
|
91
92
|
status: phaseComplete ? 'done' : 'todo',
|
|
92
|
-
}
|
|
93
|
+
};
|
|
94
|
+
if (acM && acM[1].trim()) story.acceptance = acM[1].trim();
|
|
95
|
+
stories.push(story);
|
|
93
96
|
}
|
|
94
97
|
// Fallback for pre-<task> SPRINT.md format (phases 20-30 era):
|
|
95
98
|
// "### Story 20.01.01 — title" / "### Task X — title" headings.
|
|
@@ -156,6 +159,8 @@ function scanState(rihalDir) {
|
|
|
156
159
|
|| state.raw?.project
|
|
157
160
|
|| (dirName !== '.' ? dirName : 'Unknown project');
|
|
158
161
|
|
|
162
|
+
state.projectRoot = projectDir;
|
|
163
|
+
|
|
159
164
|
state.currentPhase = state.raw?.current_phase || null;
|
|
160
165
|
state.currentSprint = state.raw?.current_sprint || null;
|
|
161
166
|
state.milestone = state.raw?.milestone || null;
|