@hanzlaa/rcode 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +7 -1
  2. package/server/dashboard.js +105 -3
  3. package/server/lib/html/client/agents-data.js +27 -0
  4. package/server/lib/html/client/app.js +15 -0
  5. package/server/lib/html/client/components/App.js +211 -0
  6. package/server/lib/html/client/components/OrchPanel.js +293 -0
  7. package/server/lib/html/client/components/Sidebar.js +73 -0
  8. package/server/lib/html/client/components/Topbar.js +53 -0
  9. package/server/lib/html/client/components/XtermPanel.js +220 -0
  10. package/server/lib/html/client/components/shared.js +330 -0
  11. package/server/lib/html/client/icons-client.js +85 -0
  12. package/server/lib/html/client/orchestrator.js +279 -0
  13. package/server/lib/html/client/preact.js +34 -0
  14. package/server/lib/html/client/store.js +91 -0
  15. package/server/lib/html/client/util.js +186 -0
  16. package/server/lib/html/client/views/AgentsView.js +83 -0
  17. package/server/lib/html/client/views/DecisionsView.js +102 -0
  18. package/server/lib/html/client/views/FilesView.js +223 -0
  19. package/server/lib/html/client/views/KanbanView.js +236 -0
  20. package/server/lib/html/client/views/MemoryView.js +157 -0
  21. package/server/lib/html/client/views/MilestonesView.js +136 -0
  22. package/server/lib/html/client/views/OrchestrationView.js +167 -0
  23. package/server/lib/html/client/views/OverviewView.js +221 -0
  24. package/server/lib/html/client/views/PhasesView.js +184 -0
  25. package/server/lib/html/client/views/RoadmapView.js +238 -0
  26. package/server/lib/html/client/views/SprintsView.js +178 -0
  27. package/server/lib/html/client/views/TasksView.js +148 -0
  28. package/server/lib/html/client.js +41 -1775
  29. package/server/lib/html/css.js +264 -44
  30. package/server/lib/html/icons.js +68 -0
  31. package/server/lib/html/shell.js +9 -296
  32. package/server/lib/scanner.js +76 -0
  33. package/server/orchestrator.js +237 -313
@@ -0,0 +1,279 @@
1
+ /**
2
+ * orchestrator.js — ESM client for the orchestrator service.
3
+ *
4
+ * Pure logic: no DOM-by-id, no innerHTML. All state flows through the
5
+ * Preact store (activeSessions field). Components import these functions
6
+ * directly; no window.* globals needed after Sprint 31.4.
7
+ *
8
+ * Constants
9
+ * ORCH_HTTP — base URL for orchestrator REST API
10
+ * ORCH_WS — base URL for orchestrator WebSocket
11
+ */
12
+
13
+ import { getState, setState } from './store.js';
14
+ import { showToast } from './components/shared.js';
15
+
16
+ export const ORCH_HTTP = 'http://localhost:7718';
17
+ export const ORCH_WS = 'ws://localhost:7718';
18
+
19
+ // ── Token helpers ─────────────────────────────────────────────────────────────
20
+
21
+ /** Return the current orchestrator token from the window global. */
22
+ export function orchToken() {
23
+ return (typeof window !== 'undefined' && window.__ORCH_TOKEN__) || '';
24
+ }
25
+
26
+ /**
27
+ * Re-fetch the live orchestrator token from the dashboard (same-origin).
28
+ * Self-heals a long-open tab if the embedded token drifts.
29
+ */
30
+ export function refreshOrchToken() {
31
+ return fetch('/api/orch-token')
32
+ .then(r => r.json())
33
+ .then(d => { if (d && d.token) window.__ORCH_TOKEN__ = d.token; })
34
+ .catch(() => {});
35
+ }
36
+
37
+ // ── REST actions ──────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * POST /api/run — start a PTY session for storyId.
41
+ * Returns the parsed JSON response (or throws on network error).
42
+ */
43
+ export function runSession(storyId, cmd) {
44
+ const tok = orchToken();
45
+ return fetch(ORCH_HTTP + '/api/run', {
46
+ method: 'POST',
47
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ storyId, cmd }),
49
+ }).then(r => r.json());
50
+ }
51
+
52
+ /**
53
+ * POST /api/stop — stop a running session.
54
+ */
55
+ export function stopSession(storyId) {
56
+ const tok = orchToken();
57
+ return fetch(ORCH_HTTP + '/api/stop', {
58
+ method: 'POST',
59
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ storyId }),
61
+ }).catch(() => {});
62
+ }
63
+
64
+ /**
65
+ * GET /api/sessions — return the sessions array (or [] on error).
66
+ */
67
+ export function fetchSessions() {
68
+ const tok = orchToken();
69
+ if (!tok) return Promise.resolve([]);
70
+ return fetch(ORCH_HTTP + '/api/sessions', {
71
+ headers: { 'Authorization': 'Bearer ' + tok },
72
+ })
73
+ .then(r => {
74
+ if (r.status === 401) { refreshOrchToken(); return []; }
75
+ return r.json().then(d => (d && d.sessions) || []);
76
+ })
77
+ .catch(() => []);
78
+ }
79
+
80
+ /**
81
+ * POST /api/clean-sessions — remove sessions older than N days.
82
+ */
83
+ export function cleanSessions(olderThanDays = 7) {
84
+ const tok = orchToken();
85
+ return fetch(ORCH_HTTP + '/api/clean-sessions', {
86
+ method: 'POST',
87
+ headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ olderThanDays }),
89
+ })
90
+ .then(r => r.json())
91
+ .catch(() => ({ removed: 0 }));
92
+ }
93
+
94
+ // ── Session-awareness helpers ─────────────────────────────────────────────────
95
+ // These read activeSessions from the store — components subscribe to the store
96
+ // and re-render when the poll writes new data.
97
+
98
+ /** Return the session object for storyId, or null. */
99
+ export function activeSession(storyId) {
100
+ const { activeSessions } = getState();
101
+ return (activeSessions || []).find(s => s.storyId === storyId) || null;
102
+ }
103
+
104
+ /** True when storyId has a session with status==='running'. */
105
+ export function isSessionRunning(storyId) {
106
+ const s = activeSession(storyId);
107
+ return !!(s && s.status === 'running');
108
+ }
109
+
110
+ /** Count running sessions touching this sprint (sprint-level + its stories). */
111
+ export function runningInSprint(sp) {
112
+ let n = isSessionRunning('sprint-' + sp.id) ? 1 : 0;
113
+ (sp.stories || []).forEach(st => { if (st.id && isSessionRunning(st.id)) n++; });
114
+ return n;
115
+ }
116
+
117
+ /** Count running sessions touching this phase. */
118
+ export function runningInPhase(p) {
119
+ let n = isSessionRunning('phase-' + p.id) ? 1 : 0;
120
+ (p.sprints || []).forEach(sp => { n += runningInSprint(sp); });
121
+ return n;
122
+ }
123
+
124
+ /** Total count of sessions with status==='running'. */
125
+ export function runningTotal() {
126
+ const { activeSessions } = getState();
127
+ return (activeSessions || []).filter(s => s.status === 'running').length;
128
+ }
129
+
130
+ // ── Session poll ──────────────────────────────────────────────────────────────
131
+
132
+ let _pollTimer = null;
133
+
134
+ /**
135
+ * Start polling /api/sessions every 4 s and writing activeSessions into the
136
+ * store. Components react via useStore(). Safe to call multiple times — only
137
+ * one poll runs at a time.
138
+ */
139
+ export function startSessionsPoll() {
140
+ if (_pollTimer) return;
141
+ _poll();
142
+ _pollTimer = setInterval(_poll, 4000);
143
+ }
144
+
145
+ /** Stop the session poll (e.g. when the dashboard is idle). */
146
+ export function stopSessionsPoll() {
147
+ if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
148
+ }
149
+
150
+ function _poll() {
151
+ fetchSessions().then(sessions => {
152
+ setState({ activeSessions: sessions });
153
+ });
154
+ }
155
+
156
+ // ── runAndOpenTerm convenience ────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Start a PTY session then open the xterm terminal panel.
160
+ * The terminal panel is driven by store state (terminal field).
161
+ *
162
+ * @param {string} storyId
163
+ * @param {string} cmd
164
+ * @param {string} title
165
+ */
166
+ export function runAndOpenTerm(storyId, cmd, title) {
167
+ // Open the panel immediately (it shows "connecting" while the session starts).
168
+ setState({
169
+ terminal: {
170
+ open: true,
171
+ storyId,
172
+ title: title || storyId,
173
+ minimized: false,
174
+ fullscreen: false,
175
+ },
176
+ });
177
+
178
+ const tok = orchToken();
179
+ if (!tok) return;
180
+
181
+ runSession(storyId, cmd).catch(() => {});
182
+ }
183
+
184
+ /**
185
+ * Open the xterm panel for an already-running session (no POST /api/run).
186
+ */
187
+ export function openTermPanel(storyId, title) {
188
+ setState({
189
+ terminal: {
190
+ open: true,
191
+ storyId,
192
+ title: title || storyId,
193
+ minimized: false,
194
+ fullscreen: false,
195
+ },
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Open the orchestrator side panel for storyId.
201
+ * Stored in store.orchPanel so OrchPanel reacts.
202
+ */
203
+ export function openOrchPanel(storyId) {
204
+ setState({ orchPanel: { open: true, storyId } });
205
+ }
206
+
207
+ /**
208
+ * runStory — Kanban "Run" action. Moves card to in_progress visually then
209
+ * delegates to runAndOpenTerm.
210
+ */
211
+ export function runStory(storyId) {
212
+ if (!storyId) return;
213
+ runAndOpenTerm(storyId, '/rihal-dev-story ' + storyId, storyId);
214
+ }
215
+
216
+ /**
217
+ * stopStory — Kanban "Stop" action.
218
+ */
219
+ export function stopStory(storyId) {
220
+ stopSession(storyId);
221
+ }
222
+
223
+ // ── Command runner ────────────────────────────────────────────────────────────
224
+ /**
225
+ * Client-side allowlist — mirrors the server COMMAND_ALLOWLIST.
226
+ * The server always re-validates; this list drives the picker dropdown only.
227
+ * Update both when adding a new command.
228
+ */
229
+ export const ALLOWED_COMMANDS = [
230
+ { cmd: '/rihal-init', label: 'init — initialise project workspace' },
231
+ { cmd: '/rihal-status', label: 'status — phase / sprint status' },
232
+ { cmd: '/rihal-progress', label: 'progress — milestone progress' },
233
+ { cmd: '/rihal-help', label: 'help — command reference' },
234
+ { cmd: '/rihal-health', label: 'health — repo health check' },
235
+ { cmd: '/rihal-next', label: 'next — suggest next action' },
236
+ { cmd: '/rihal-show', label: 'show — show current plan' },
237
+ { cmd: '/rihal-list-plans', label: 'list-plans — list all sprint plans' },
238
+ { cmd: '/rihal-sprint-status', label: 'sprint-status — sprint execution status' },
239
+ { cmd: '/rihal-config', label: 'config — show rihal config' },
240
+ { cmd: '/rihal-diff', label: 'diff — diff since last checkpoint' },
241
+ { cmd: '/rihal-stats', label: 'stats — project statistics' },
242
+ ];
243
+
244
+ /**
245
+ * Launch an allowlisted rihal command from the dashboard command runner.
246
+ * Uses a synthetic storyId derived from the command slug so it shows up as
247
+ * its own session in the Orchestration grid.
248
+ *
249
+ * storyId format: "cmd-rihal-init" (satisfies STORY_ID_RE /^[A-Za-z0-9._-]+$/).
250
+ *
251
+ * Surfaces errors as toast notifications:
252
+ * - 403 / 503 / any server error message → showToast('Command error: ...')
253
+ * - 409 "already running" → no toast (expected; terminal reattaches)
254
+ * - network failure → showToast('Could not reach orchestrator')
255
+ *
256
+ * @param {string} cmd Must be one of ALLOWED_COMMANDS[*].cmd.
257
+ */
258
+ export function runCommandFromUI(cmd) {
259
+ if (!cmd) return;
260
+ const slug = cmd.replace(/^\//, '').replace(/\//g, '-');
261
+ const storyId = 'cmd-' + slug;
262
+ const title = cmd + ' (command runner)';
263
+ // Open the terminal panel immediately so the user gets visual feedback.
264
+ setState({
265
+ terminal: { open: true, storyId, title, minimized: false, fullscreen: false },
266
+ });
267
+
268
+ const tok = orchToken();
269
+ if (!tok) { showToast('No orchestrator token — restart the dashboard'); return; }
270
+
271
+ runSession(storyId, cmd)
272
+ .then(data => {
273
+ // 409 = already running (not an error — terminal is already attached).
274
+ if (data && data.error && !data.error.includes('already running')) {
275
+ showToast('Command error: ' + data.error);
276
+ }
277
+ })
278
+ .catch(() => showToast('Could not reach orchestrator'));
279
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Preact + htm ESM runtime — single dependency surface.
3
+ *
4
+ * All esm.sh version pins live here. Every other client module imports
5
+ * from this file so version bumps happen in one place.
6
+ *
7
+ * Pinned versions (current stable as of 2026-05):
8
+ * preact 10.24.3
9
+ * htm 3.1.1
10
+ */
11
+
12
+ import { h, render, Fragment } from 'https://esm.sh/preact@10.24.3';
13
+ import {
14
+ useState,
15
+ useEffect,
16
+ useRef,
17
+ useMemo,
18
+ useCallback,
19
+ useReducer,
20
+ } from 'https://esm.sh/preact@10.24.3/hooks';
21
+ import htmLib from 'https://esm.sh/htm@3.1.1';
22
+
23
+ // htm bound to Preact's h — use as a tagged template literal: html`<div>...</div>`
24
+ export const html = htmLib.bind(h);
25
+
26
+ export { h, render, Fragment };
27
+ export {
28
+ useState,
29
+ useEffect,
30
+ useRef,
31
+ useMemo,
32
+ useCallback,
33
+ useReducer,
34
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Reactive store — shared state for the Preact dashboard client.
3
+ *
4
+ * Seeds from window.__S__ (injected by client.js before app.js loads).
5
+ * Expose: getState(), setState(patch), subscribe(fn), useStore() hook.
6
+ *
7
+ * This is intentionally NOT a framework — no Redux, no Zustand.
8
+ * Just a plain mutable object, a subscriber set, and a Preact hook.
9
+ */
10
+
11
+ import { useState, useEffect } from './preact.js';
12
+
13
+ // ---- Initial state seed from server-injected data ----
14
+ const _seed = (typeof window !== 'undefined' && window.__S__) || {};
15
+
16
+ let _state = {
17
+ // Fields injected by client.js / window.__S__
18
+ phases: _seed.phases || [],
19
+ milestone: _seed.milestone || '',
20
+ currentPhase: _seed.currentPhase || null,
21
+ currentSprint: _seed.currentSprint || null,
22
+ decisions: _seed.decisions || [],
23
+ blockers: _seed.blockers || [],
24
+ council_sessions: _seed.council_sessions || [],
25
+ last_session: _seed.last_session || null,
26
+ chains: _seed.chains || [],
27
+ workstreams: _seed.workstreams || [],
28
+ pendingHandoff: _seed.pendingHandoff || null,
29
+ memoryBank: _seed.memoryBank || null,
30
+ // Live orchestrator sessions (populated by startSessionsPoll in orchestrator.js)
31
+ activeSessions: [],
32
+ // File jump bridge: AgentsView sets this to a slug so FilesView opens it.
33
+ requestedFile: null,
34
+ // xterm terminal panel state (driven by orchestrator.js / XtermPanel.js)
35
+ // { open, storyId, title, minimized, fullscreen }
36
+ terminal: null,
37
+ // Orchestrator side-panel state (driven by orchestrator.js / OrchPanel.js)
38
+ // { open, storyId }
39
+ orchPanel: null,
40
+ };
41
+
42
+ /** Registered subscriber functions. */
43
+ const _subscribers = new Set();
44
+
45
+ /** Return a shallow copy of the current state. */
46
+ export function getState() {
47
+ return { ..._state };
48
+ }
49
+
50
+ /**
51
+ * Shallow-merge `patch` into state, then notify all subscribers.
52
+ * Only notifies if at least one key actually changed value.
53
+ */
54
+ export function setState(patch) {
55
+ let changed = false;
56
+ for (const key of Object.keys(patch)) {
57
+ if (_state[key] !== patch[key]) {
58
+ changed = true;
59
+ break;
60
+ }
61
+ }
62
+ if (!changed) return;
63
+ _state = { ..._state, ...patch };
64
+ for (const fn of _subscribers) {
65
+ try { fn(_state); } catch (e) { console.error('[store] subscriber error', e); }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Subscribe to state changes. Returns an unsubscribe function.
71
+ * @param {function} fn — called with the new state on every setState().
72
+ */
73
+ export function subscribe(fn) {
74
+ _subscribers.add(fn);
75
+ return () => _subscribers.delete(fn);
76
+ }
77
+
78
+ /**
79
+ * Preact hook. Subscribes the calling component to the store and
80
+ * returns the current state. The component re-renders on every setState().
81
+ */
82
+ export function useStore() {
83
+ const [state, setLocalState] = useState(getState);
84
+ useEffect(() => {
85
+ // Resync on mount in case setState was called before mount.
86
+ setLocalState(getState());
87
+ const unsub = subscribe(newState => setLocalState({ ...newState }));
88
+ return unsub;
89
+ }, []);
90
+ return state;
91
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Shared pure helpers — ported from client-render.js.
3
+ *
4
+ * All functions are stateless: no module-global phase state (_phases).
5
+ * `allSprints` and `allTasks` take `phases` as an explicit argument.
6
+ *
7
+ * Import here; do NOT duplicate in component files.
8
+ */
9
+
10
+ /** HTML-escape a value for safe rendering. */
11
+ export function esc(s) {
12
+ return String(s || '')
13
+ .replace(/&/g, '&amp;')
14
+ .replace(/</g, '&lt;')
15
+ .replace(/>/g, '&gt;')
16
+ .replace(/"/g, '&quot;')
17
+ .replace(/'/g, '&#39;');
18
+ }
19
+
20
+ /** Percentage string. Returns '—' if total is 0. */
21
+ export function pct(done, total) {
22
+ return total > 0 ? Math.round(done / total * 100) + '%' : '—';
23
+ }
24
+
25
+ /** Percentage as a number (0–100). Returns 0 if total is 0. */
26
+ export function pctNum(done, total) {
27
+ return total > 0 ? Math.round(done / total * 100) : 0;
28
+ }
29
+
30
+ /** Slice an ISO date string to YYYY-MM-DD, or null. */
31
+ export function dateStr(s) {
32
+ return s ? String(s).slice(0, 10) : null;
33
+ }
34
+
35
+ /** Human-readable date string, e.g. "May 16, 2026". */
36
+ export function humanDate(s) {
37
+ if (!s) return null;
38
+ try {
39
+ const d = new Date(s);
40
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
41
+ } catch {
42
+ return dateStr(s);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Flatten all sprints across phases.
48
+ * @param {Array} phases — the phases array from the store.
49
+ * @returns {Array} sprints with phaseId and phaseName injected.
50
+ */
51
+ export function allSprints(phases) {
52
+ return (phases || []).flatMap(p =>
53
+ (p.sprints || []).map(s => Object.assign({}, s, { phaseId: p.id, phaseName: p.name }))
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Flatten all tasks (stories) across phases and sprints.
59
+ * @param {Array} phases — the phases array from the store.
60
+ * @returns {Array} tasks with sprintId, phaseId, and phaseName injected.
61
+ */
62
+ export function allTasks(phases) {
63
+ return (phases || []).flatMap(p =>
64
+ (p.sprints || []).flatMap(s =>
65
+ (s.stories || []).map(t => Object.assign({}, t, { sprintId: s.id, phaseId: p.id, phaseName: p.name }))
66
+ )
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Return a status chip descriptor — NOT an HTML string.
72
+ * Components decide how to render the CSS class and label.
73
+ *
74
+ * @param {string} status
75
+ * @returns {{ cls: string, label: string }}
76
+ */
77
+ export function chip(status) {
78
+ const s = String(status || '').toLowerCase();
79
+ const cls =
80
+ (s === 'complete' || s === 'completed' || s === 'done') ? 'complete' :
81
+ (s === 'active' || s === 'in_progress') ? 'active' :
82
+ s === 'blocked' ? 'blocked' :
83
+ s === 'planned' ? 'planned' :
84
+ s === 'todo' ? 'todo' : 'other';
85
+ return { cls, label: status };
86
+ }
87
+
88
+ /**
89
+ * Human-readable elapsed time since an ISO timestamp.
90
+ * Ported from _orchElapsed() in client-main.js.
91
+ *
92
+ * @param {string|null} iso — ISO 8601 start time
93
+ * @returns {string}
94
+ */
95
+ export function orchElapsed(iso) {
96
+ if (!iso) return '—';
97
+ let s = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
98
+ if (s < 0) s = 0;
99
+ if (s < 60) return s + 's';
100
+ const m = Math.floor(s / 60);
101
+ if (m < 60) return m + 'm ' + (s % 60) + 's';
102
+ return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
103
+ }
104
+
105
+ /**
106
+ * Return command-hint pairs [cmd, desc] for a sprint, based on its status.
107
+ * Ported from sprintHints() in client-render.js.
108
+ *
109
+ * @param {object} s — sprint object (id, status, stories, phaseId)
110
+ * @returns {Array<[string, string]>}
111
+ */
112
+ export function sprintHints(s) {
113
+ const stories = Array.isArray(s.stories) ? s.stories : [];
114
+ const st = s.status || 'planned';
115
+ const sid = s.id || '';
116
+ if (st === 'completed' || st === 'complete' || st === 'done') {
117
+ return [
118
+ ['/rihal-verify-work', 'Verify UAT for Sprint ' + sid],
119
+ ['/rihal-audit', 'Audit completed Sprint ' + sid],
120
+ ['/rihal-session-report','Generate session report'],
121
+ ['/rihal-code-review', 'Review code from Sprint ' + sid],
122
+ ];
123
+ } else if (st === 'active' || st === 'in_progress') {
124
+ return [
125
+ ['/rihal-progress', 'Check Sprint ' + sid + ' progress'],
126
+ ['/rihal-sprint-status','Status report for Sprint ' + sid],
127
+ ['/rihal-pause-work', 'Pause and save context'],
128
+ ];
129
+ } else if (st === 'blocked') {
130
+ return [
131
+ ['/rihal-debug', 'Debug blocker in Sprint ' + sid],
132
+ ['/rihal-correct-course','Course-correct Sprint ' + sid],
133
+ ];
134
+ } else {
135
+ if (!stories.length) {
136
+ return [
137
+ ['/rihal-sprint-planning','Groom Sprint ' + sid + ' — add stories'],
138
+ ['/rihal-create-story', 'Create a story for Sprint ' + sid],
139
+ ['/rihal-discuss-phase', 'Discuss approach before planning'],
140
+ ];
141
+ }
142
+ return [
143
+ ['/rihal-execute-sprint ' + sid, 'Execute Sprint ' + sid],
144
+ ['/rihal-discuss-phase', 'Discuss before executing'],
145
+ ['/rihal-sprint-planning','Refine Sprint ' + sid + ' plan'],
146
+ ];
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Return command-hint pairs [cmd, desc] for a phase, based on its status.
152
+ * Ported from phaseHints() in client-render.js.
153
+ *
154
+ * @param {object} p — phase object (id, status, sprints)
155
+ * @returns {Array<[string, string]>}
156
+ */
157
+ export function phaseHints(p) {
158
+ const sps = Array.isArray(p.sprints) ? p.sprints : [];
159
+ const st = p.status || 'planned';
160
+ const pid = p.id || '';
161
+ if (st === 'completed' || st === 'complete' || st === 'done') {
162
+ return [
163
+ ['/rihal-validate-phase','Validate Phase ' + pid + ' deliverables'],
164
+ ['/rihal-audit', 'Audit Phase ' + pid + ' completion'],
165
+ ['/rihal-code-review', 'Review Phase ' + pid + ' code'],
166
+ ];
167
+ } else if (st === 'active' || st === 'in_progress') {
168
+ return [
169
+ ['/rihal-progress', 'Check Phase ' + pid + ' progress'],
170
+ ['/rihal-sprint-status','Current sprint status'],
171
+ ['/rihal-code-review', 'Review code in Phase ' + pid],
172
+ ];
173
+ } else {
174
+ if (!sps.length) {
175
+ return [
176
+ ['/rihal-plan', 'Create sprint plan for Phase ' + pid],
177
+ ['/rihal-discuss-phase','Discuss Phase ' + pid + ' approach'],
178
+ ['/rihal-research-phase','Research Phase ' + pid + ' before planning'],
179
+ ];
180
+ }
181
+ return [
182
+ ['/rihal-execute ' + pid, 'Start executing Phase ' + pid],
183
+ ['/rihal-sprint-planning','Plan next sprint in Phase ' + pid],
184
+ ];
185
+ }
186
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * AgentsView — Preact port of the #view-agents markup from shell.js.
3
+ *
4
+ * Renders Team (real agents) and AI Agents groups from the AGENTS roster
5
+ * that was moved client-side into agents-data.js.
6
+ *
7
+ * Clicking an agent sets store.requestedFile = skillSlug and navigates to
8
+ * the Files view, replacing the legacy viewAgentSkill() setTimeout+DOM-poll
9
+ * hack in shell.js. FilesView watches requestedFile and pre-fills the filter.
10
+ */
11
+
12
+ import { html, useState } from '../preact.js';
13
+ import { setState } from '../store.js';
14
+ import { AGENTS } from '../agents-data.js';
15
+
16
+ const REAL_AGENTS = AGENTS.filter(a => a.real);
17
+ const AI_AGENTS = AGENTS.filter(a => !a.real);
18
+
19
+ // ---- Single agent card ----
20
+ function AgentCard({ agent }) {
21
+ const skillSlug = agent.name.split(' ')[0].toLowerCase();
22
+
23
+ function handleClick() {
24
+ // Navigate to Files view and pre-fill search with the agent's skill slug.
25
+ // FilesView watches requestedFile in the store and responds via useEffect.
26
+ setState({ requestedFile: skillSlug });
27
+ window.location.hash = 'files';
28
+ }
29
+
30
+ return html`
31
+ <div
32
+ class="agent-card"
33
+ onClick=${handleClick}
34
+ style="cursor:pointer;"
35
+ >
36
+ <div class="name">
37
+ ${agent.name}
38
+ ${agent.real ? html` <span class="real-badge">real</span>` : null}
39
+ ${' '}<span class="type-badge">${agent.type}</span>
40
+ </div>
41
+ <div class="arabic">${agent.arabic}</div>
42
+ <div class="role">${agent.role}</div>
43
+ </div>
44
+ `;
45
+ }
46
+
47
+ // ---- Agent group ----
48
+ function AgentGroup({ label, agents, filter }) {
49
+ const visible = agents.filter(a => {
50
+ if (!filter) return true;
51
+ const q = filter.toLowerCase();
52
+ return (a.name + ' ' + a.role + ' ' + a.arabic + ' ' + a.type).toLowerCase().includes(q);
53
+ });
54
+ if (!visible.length) return null;
55
+ return html`
56
+ <div class="memory-group-header">${label} (${visible.length})</div>
57
+ <div class="agent-list">
58
+ ${visible.map(a => html`<${AgentCard} key=${a.name} agent=${a} />`)}
59
+ </div>
60
+ `;
61
+ }
62
+
63
+ // ---- Root AgentsView ----
64
+ export function AgentsView() {
65
+ const [filter, setFilter] = useState('');
66
+
67
+ return html`
68
+ <div class="view active" id="view-agents">
69
+ <div class="view-title">Team</div>
70
+ <div class="filter-bar">
71
+ <input
72
+ class="filter-input"
73
+ type="text"
74
+ placeholder="Filter agents…"
75
+ value=${filter}
76
+ onInput=${e => setFilter(e.target.value)}
77
+ />
78
+ </div>
79
+ <${AgentGroup} label="Team" agents=${REAL_AGENTS} filter=${filter} />
80
+ <${AgentGroup} label="AI Agents" agents=${AI_AGENTS} filter=${filter} />
81
+ </div>
82
+ `;
83
+ }