@hanzlaa/rcode 4.1.2 → 4.3.1

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 (70) 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/rcode/references/auto-init-guard.md +2 -2
  14. package/rcode/references/output-format.md +5 -5
  15. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-10-complete.md +1 -1
  16. package/server/dashboard.js +33 -13
  17. package/server/lib/api.js +62 -4
  18. package/server/lib/html/client/agents-data.js +22 -18
  19. package/server/lib/html/client/app.js +3 -0
  20. package/server/lib/html/client/components/AgentCard.js +127 -0
  21. package/server/lib/html/client/components/App.js +104 -39
  22. package/server/lib/html/client/components/CommandPalette.js +133 -0
  23. package/server/lib/html/client/components/FileReader.js +116 -0
  24. package/server/lib/html/client/components/FilterChips.js +94 -0
  25. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  26. package/server/lib/html/client/components/OrchPanel.js +80 -52
  27. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  28. package/server/lib/html/client/components/RejectDialog.js +78 -0
  29. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  30. package/server/lib/html/client/components/Sidebar.js +106 -61
  31. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  32. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  33. package/server/lib/html/client/components/Topbar.js +86 -39
  34. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  35. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  36. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  37. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  38. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  39. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  40. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  41. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  42. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  43. package/server/lib/html/client/components/shared.js +47 -11
  44. package/server/lib/html/client/filter-state.js +72 -0
  45. package/server/lib/html/client/icons-client.js +7 -0
  46. package/server/lib/html/client/notify.js +75 -0
  47. package/server/lib/html/client/orchestrator.js +168 -41
  48. package/server/lib/html/client/preact.js +13 -8
  49. package/server/lib/html/client/store.js +70 -6
  50. package/server/lib/html/client/util.js +78 -0
  51. package/server/lib/html/client/vendor/htm.js +1 -0
  52. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  53. package/server/lib/html/client/vendor/preact.js +2 -0
  54. package/server/lib/html/client/views/AgentsView.js +144 -51
  55. package/server/lib/html/client/views/FilesView.js +20 -103
  56. package/server/lib/html/client/views/KanbanView.js +40 -21
  57. package/server/lib/html/client/views/MemoryView.js +26 -9
  58. package/server/lib/html/client/views/MilestonesView.js +4 -4
  59. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  60. package/server/lib/html/client/views/OverviewView.js +47 -239
  61. package/server/lib/html/client/views/PhasesView.js +50 -6
  62. package/server/lib/html/client/views/RoadmapView.js +6 -3
  63. package/server/lib/html/client/views/SprintsView.js +50 -6
  64. package/server/lib/html/client/views/TasksView.js +4 -3
  65. package/server/lib/html/client.js +21 -4
  66. package/server/lib/html/css.js +2761 -8
  67. package/server/lib/html/icons.js +7 -0
  68. package/server/lib/html/shell.js +10 -3
  69. package/server/lib/scanner.js +376 -39
  70. package/server/orchestrator.js +346 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "4.1.2",
3
+ "version": "4.3.1",
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": {
@@ -25,8 +25,8 @@ rcode isn't configured for this project yet. Let me set it up — takes 30 secon
25
25
  **1. Bootstrap local tooling** — copy bin from the global install:
26
26
 
27
27
  ```bash
28
- GLOBAL_RIHAL="$HOME/.rcode"
29
- TOOLS_SRC="$GLOBAL_RIHAL/bin/rcode-tools.cjs"
28
+ GLOBAL_RCODE="$HOME/.rcode"
29
+ TOOLS_SRC="$GLOBAL_RCODE/bin/rcode-tools.cjs"
30
30
 
31
31
  if [ ! -f "$TOOLS_SRC" ]; then
32
32
  echo "ERROR: Global rcode tools not found at $TOOLS_SRC"
@@ -42,7 +42,7 @@ Use for major workflow transitions.
42
42
 
43
43
  ```
44
44
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
45
- RIHAL ► {STAGE NAME}
45
+ RCODE ► {STAGE NAME}
46
46
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47
47
  ```
48
48
 
@@ -69,7 +69,7 @@ Use this when a router command dispatches to another command:
69
69
 
70
70
  ```
71
71
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
- RIHAL ► ROUTING
72
+ RCODE ► ROUTING
73
73
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
74
 
75
75
  Input: {user's question or intent}
@@ -328,7 +328,7 @@ Use standard markdown pipe tables with status symbols:
328
328
  **Majlis banner** (multi-agent council):
329
329
  ```
330
330
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
331
- RIHAL ► MAJLIS CONVENING
331
+ RCODE ► MAJLIS CONVENING
332
332
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
333
333
  ```
334
334
 
@@ -354,7 +354,7 @@ the banner, not inside it.
354
354
 
355
355
  ```
356
356
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
357
- RIHAL ► PLANNING SPRINT 01.1
357
+ RCODE ► PLANNING SPRINT 01.1
358
358
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
359
359
  التخطيط للسباق 01.1 — يرجى الانتظار
360
360
  ```
@@ -389,7 +389,7 @@ translated prose goes outside the art, on its own line(s).
389
389
 
390
390
  - Varying box/banner widths within same output
391
391
  - Mixing banner styles (`===`, `---`, `***`)
392
- - Skipping `RIHAL ►` prefix in stage banners
392
+ - Skipping `RCODE ►` prefix in stage banners
393
393
  - Random emoji (`🚀`, `✨`, `💫`) outside the approved set
394
394
  - Missing Next Up block after workflow completions
395
395
  - Hardcoding references to other methodologies in rcode's UX
@@ -16,7 +16,7 @@ Append `step-10-complete` to `stepsCompleted`. Add `completedAt: {ISO date}`.
16
16
 
17
17
  ```
18
18
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19
- RIHAL ► ROADMAP CREATED
19
+ RCODE ► ROADMAP CREATED
20
20
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21
21
 
22
22
  Source PRD: {inputFile}
@@ -31,7 +31,7 @@ const { spawn } = require('child_process');
31
31
  const CLIENT_DIR = path.join(__dirname, 'lib', 'html', 'client');
32
32
 
33
33
  const { scanState } = require('./lib/scanner');
34
- const { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory } = require('./lib/api');
34
+ const { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory, handleApiAgents } = require('./lib/api');
35
35
  const { renderHtml } = require('./lib/html/shell');
36
36
 
37
37
  // ---------- Configuration ----------
@@ -60,7 +60,20 @@ function loadOrchToken() {
60
60
  const ORCH_TOKEN = loadOrchToken();
61
61
 
62
62
  // ---------- HTTP Server ----------
63
+ // Every request runs through a try/catch so an unanticipated throw inside a
64
+ // handler (e.g. a pathological .planning tree in the scanner) returns a 500
65
+ // instead of crashing the whole server process.
63
66
  const server = http.createServer((req, res) => {
67
+ try {
68
+ handleRequest(req, res);
69
+ } catch (err) {
70
+ console.error('[dashboard] request handler failed:', err && err.stack || err);
71
+ if (!res.headersSent) res.writeHead(500, { 'Content-Type': 'text/plain' });
72
+ res.end('Internal server error');
73
+ }
74
+ });
75
+
76
+ function handleRequest(req, res) {
64
77
  const url = req.url || '/';
65
78
 
66
79
  if (url === '/health') {
@@ -79,6 +92,11 @@ const server = http.createServer((req, res) => {
79
92
  return;
80
93
  }
81
94
 
95
+ if (url === '/api/agents') {
96
+ handleApiAgents(req, res, PROJECT_ROOT);
97
+ return;
98
+ }
99
+
82
100
  if (url.startsWith('/api/file')) {
83
101
  handleApiFile(req, res, PROJECT_ROOT);
84
102
  return;
@@ -104,10 +122,12 @@ const server = http.createServer((req, res) => {
104
122
 
105
123
  if (url.startsWith('/js/')) {
106
124
  const name = url.slice(4).split('?')[0];
107
- // Allow exactly one optional subdirectory (e.g. components/App.js, views/Foo.js)
108
- // while still rejecting traversal attempts. The regex blocks `..`, encoded
109
- // separators, and anything other than word chars, dots, hyphens, and one `/`.
110
- if (!/^(?:[\w.-]+\/)?[\w.-]+\.js$/.test(name)) { res.writeHead(404); res.end('Not found'); return; }
125
+ // Allow nested subdirectories (e.g. components/App.js, views/Foo.js,
126
+ // components/dashboard/ProgressDonut.js) while still rejecting traversal.
127
+ // The regex limits each segment to word chars, dots, and hyphens; the
128
+ // resolved-path check below is the real traversal guard (a `..` segment
129
+ // would pass this pattern but fail the CLIENT_DIR containment check).
130
+ if (!/^(?:[\w.-]+\/)*[\w.-]+\.js$/.test(name)) { res.writeHead(404); res.end('Not found'); return; }
111
131
  // Defense-in-depth: resolved path must stay inside CLIENT_DIR even after
112
132
  // any OS-level resolution (handles encoded traversal the regex might miss).
113
133
  const resolved = path.resolve(CLIENT_DIR, name);
@@ -135,17 +155,17 @@ const server = http.createServer((req, res) => {
135
155
 
136
156
  res.writeHead(404);
137
157
  res.end('Not found');
138
- });
158
+ }
139
159
 
140
160
  server.listen(PORT, '127.0.0.1', () => {
141
161
  console.log(`\n🕌 Majlis (مجلس) — rcode Dashboard`);
142
162
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
143
- console.log(` Mode: view-only`);
144
- console.log(` URL: http://localhost:${PORT}`);
145
- console.log(` Scanning: ${RCODE_DIR}`);
146
- console.log(` Refresh: 30s soft poll`);
147
- console.log(` Keys: R=refresh 1-9=views F=filter`);
148
- console.log(` Stop: kill $(ss -ltnp 'sport = :${PORT}' | awk 'NR>1{match($6,/pid=([0-9]+)/,m); print m[1]}')`);
163
+ console.log(` 👉 OPEN THIS: http://localhost:${PORT}`);
164
+ console.log(` Mode: view-only`);
165
+ console.log(` Scanning: ${RCODE_DIR}`);
166
+ console.log(` Refresh: 30s soft poll`);
167
+ console.log(` Note: port ${PORT + 1} is the internal orchestrator API — not for the browser`);
168
+ console.log(` Stop: kill $(ss -ltnp 'sport = :${PORT}' | awk 'NR>1{match($6,/pid=([0-9]+)/,m); print m[1]}')`);
149
169
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
150
170
  });
151
171
 
@@ -205,7 +225,7 @@ function spawnOrchestrator() {
205
225
  try {
206
226
  _orchProc = spawn(process.execPath, [ORCH_BIN], {
207
227
  cwd: path.join(__dirname, '..'),
208
- env: { ...process.env, ORCH_TOKEN, RCODE_DIR, PROJECT_ROOT },
228
+ env: { ...process.env, ORCH_TOKEN, RCODE_DIR, PROJECT_ROOT, DASH_PORT: String(PORT) },
209
229
  stdio: 'pipe',
210
230
  });
211
231
  _orchProc.stdout.on('data', chunk => {
package/server/lib/api.js CHANGED
@@ -8,7 +8,8 @@ const { scanState, scanMemoryBank } = require('./scanner');
8
8
  function handleApiState(req, res, rcodeDir) {
9
9
  const state = scanState(rcodeDir);
10
10
  res.writeHead(200, { 'Content-Type': 'application/json' });
11
- res.end(JSON.stringify(state, null, 2));
11
+ // Compact JSON pretty-printing roughly doubled the polled payload.
12
+ res.end(JSON.stringify(state));
12
13
  }
13
14
 
14
15
  function handleApiFiles(req, res, projectRoot) {
@@ -192,13 +193,70 @@ function handleApiHierarchy(req, res, rcodeDir) {
192
193
  })),
193
194
  };
194
195
  res.writeHead(200, { 'Content-Type': 'application/json' });
195
- res.end(JSON.stringify(hierarchy, null, 2));
196
+ res.end(JSON.stringify(hierarchy));
196
197
  }
197
198
 
198
199
  function handleApiMemory(req, res, rcodeDir) {
199
200
  const memory = scanMemoryBank(rcodeDir);
200
201
  res.writeHead(200, { 'Content-Type': 'application/json' });
201
- res.end(JSON.stringify(memory, null, 2));
202
+ res.end(JSON.stringify(memory));
202
203
  }
203
204
 
204
- module.exports = { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory };
205
+ // Parse the keys we surface on agent cards out of an agent definition's
206
+ // YAML frontmatter. Deliberately not a YAML parser: top-level `key: value`
207
+ // scalar lines are read directly; for `description` (usually a `|` block
208
+ // scalar) the first two indented lines are captured as a card-sized summary.
209
+ function parseAgentFrontmatter(raw) {
210
+ const meta = { name: null, model: null, tools: [], color: null, description: null };
211
+ if (!raw.startsWith('---')) return meta;
212
+ const end = raw.indexOf('\n---', 3);
213
+ if (end === -1) return meta;
214
+ let inDescription = false;
215
+ const descLines = [];
216
+ for (const line of raw.slice(3, end).split('\n')) {
217
+ const m = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
218
+ if (m) {
219
+ inDescription = false;
220
+ const key = m[1].toLowerCase();
221
+ const value = m[2].trim();
222
+ if (key === 'description') {
223
+ if (value && value !== '|' && value !== '>') descLines.push(value);
224
+ else inDescription = true;
225
+ continue;
226
+ }
227
+ if (!value || value === '|' || value === '>') continue;
228
+ if (key === 'name') meta.name = value;
229
+ if (key === 'model') meta.model = value;
230
+ if (key === 'color') meta.color = value;
231
+ if (key === 'tools') meta.tools = value.split(',').map(t => t.trim()).filter(Boolean);
232
+ } else if (inDescription && descLines.length < 2 && /^\s+\S/.test(line)) {
233
+ descLines.push(line.trim());
234
+ }
235
+ }
236
+ meta.description = descLines.join(' ') || null;
237
+ return meta;
238
+ }
239
+
240
+ // Read-only roster metadata for the Agents view. Scans the fixed
241
+ // rcode/agents/ directory (no user-supplied paths — nothing to contain) and
242
+ // returns one small frontmatter summary per agent .md file. Full prompt
243
+ // bodies are NOT included; the client fetches those lazily per agent via the
244
+ // existing /api/file handler when a card is opened.
245
+ function handleApiAgents(req, res, projectRoot) {
246
+ const agentsDir = path.join(projectRoot, 'rcode', 'agents');
247
+ let entries = [];
248
+ try { entries = fs.readdirSync(agentsDir, { withFileTypes: true }); }
249
+ catch { /* no agents dir — return an empty roster */ }
250
+ const agents = [];
251
+ for (const e of entries) {
252
+ if (!e.isFile() || !e.name.endsWith('.md')) continue;
253
+ let raw;
254
+ try { raw = fs.readFileSync(path.join(agentsDir, e.name), 'utf8'); }
255
+ catch { continue; }
256
+ agents.push({ file: e.name, ...parseAgentFrontmatter(raw) });
257
+ }
258
+ res.writeHead(200, { 'Content-Type': 'application/json' });
259
+ res.end(JSON.stringify(agents));
260
+ }
261
+
262
+ module.exports = { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory, handleApiAgents };
@@ -3,25 +3,29 @@
3
3
  *
4
4
  * Previously lived in shell.js:17-36 as a server-rendered array.
5
5
  * Now exported as a pure ESM constant so AgentsView can render it.
6
+ *
7
+ * `file` is the agent's definition under rcode/agents/ — fetched lazily by
8
+ * AgentsView when a card is opened. null = no prompt file on disk (system
9
+ * entries like Raees/Majlis/Diwan are skills, not agent definitions).
6
10
  */
7
11
 
8
12
  export const AGENTS = [
9
- { name: 'Sadiq Damani', arabic: 'صادق', role: 'Director of Strategy', real: true, type: 'leadership' },
10
- { name: 'Waleed Al Harthi', arabic: 'وليد', role: 'CTO', real: true, type: 'leadership' },
11
- { name: 'Ahmed Al Hassani', arabic: 'أحمد الحسني', role: 'Technology & Development Director', real: true, type: 'leadership' },
12
- { name: 'Nasser', arabic: 'ناصر', role: 'Engineering Manager', real: true, type: 'leadership' },
13
- { name: 'Hussain', arabic: 'حسين', role: 'PM + Scrum Master', type: 'product' },
14
- { name: 'Layla', arabic: 'ليلى', role: 'Lead UX Designer', type: 'design' },
15
- { name: 'Zahra', arabic: 'زهرة', role: 'Branding & Creative Director', type: 'design' },
16
- { name: 'Omar', arabic: 'عمر', role: 'Full-Stack Engineer', type: 'engineering' },
17
- { name: 'Haitham Al Khamiyasi', arabic: 'هيثم', role: 'Senior Frontend', real: true, type: 'engineering' },
18
- { name: 'Yousef', arabic: 'يوسف', role: 'Senior Backend', type: 'engineering' },
19
- { name: 'Zayd', arabic: 'زيد', role: 'ML Engineer', type: 'engineering' },
20
- { name: 'Fatima', arabic: 'فاطمة', role: 'QA Lead', type: 'quality' },
21
- { name: 'Khalid', arabic: 'خالد', role: 'DevOps', type: 'engineering' },
22
- { name: 'Noor', arabic: 'نور', role: 'Scribe', type: 'support' },
23
- { name: 'Mariam', arabic: 'مريم', role: 'Marketing Lead', type: 'product' },
24
- { name: 'Raees', arabic: 'رئيس', role: 'Orchestration Director', type: 'system' },
25
- { name: 'Majlis', arabic: 'مجلس', role: 'Consulting Council', type: 'system' },
26
- { name: 'Diwan', arabic: 'ديوان', role: 'Dashboard Registry', type: 'system' },
13
+ { name: 'Sadiq Damani', arabic: 'صادق', role: 'Director of Strategy', real: true, type: 'leadership', file: 'rcode-sadiq.md' },
14
+ { name: 'Waleed Al Harthi', arabic: 'وليد', role: 'CTO', real: true, type: 'leadership', file: 'rcode-waleed.md' },
15
+ { name: 'Ahmed Al Hassani', arabic: 'أحمد الحسني', role: 'Technology & Development Director', real: true, type: 'leadership', file: 'rcode-ahmed.md' },
16
+ { name: 'Nasser', arabic: 'ناصر', role: 'Engineering Manager', real: true, type: 'leadership', file: 'rcode-nasser.md' },
17
+ { name: 'Hussain', arabic: 'حسين', role: 'PM + Scrum Master', type: 'product', file: 'rcode-hussain-pm.md' },
18
+ { name: 'Layla', arabic: 'ليلى', role: 'Lead UX Designer', type: 'design', file: 'rcode-layla.md' },
19
+ { name: 'Zahra', arabic: 'زهرة', role: 'Branding & Creative Director', type: 'design', file: 'rcode-zahra.md' },
20
+ { name: 'Omar', arabic: 'عمر', role: 'Full-Stack Engineer', type: 'engineering', file: 'rcode-omar.md' },
21
+ { name: 'Haitham Al Khamiyasi', arabic: 'هيثم', role: 'Senior Frontend', real: true, type: 'engineering', file: 'rcode-haitham.md' },
22
+ { name: 'Yousef', arabic: 'يوسف', role: 'Senior Backend', type: 'engineering', file: 'rcode-yousef.md' },
23
+ { name: 'Zayd', arabic: 'زيد', role: 'ML Engineer', type: 'engineering', file: 'rcode-zayd.md' },
24
+ { name: 'Fatima', arabic: 'فاطمة', role: 'QA Lead', type: 'quality', file: 'rcode-fatima.md' },
25
+ { name: 'Khalid', arabic: 'خالد', role: 'DevOps', type: 'engineering', file: 'rcode-khalid.md' },
26
+ { name: 'Noor', arabic: 'نور', role: 'Scribe', type: 'support', file: 'rcode-noor.md' },
27
+ { name: 'Mariam', arabic: 'مريم', role: 'Marketing Lead', type: 'product', file: 'rcode-mariam.md' },
28
+ { name: 'Raees', arabic: 'رئيس', role: 'Orchestration Director', type: 'system', file: null },
29
+ { name: 'Majlis', arabic: 'مجلس', role: 'Consulting Council', type: 'system', file: null },
30
+ { name: 'Diwan', arabic: 'ديوان', role: 'Dashboard Registry', type: 'system', file: null },
27
31
  ];
@@ -11,5 +11,8 @@ import { App } from './components/App.js';
11
11
 
12
12
  const root = document.getElementById('app-root');
13
13
  if (root) {
14
+ // Drop the SSR loading shell — Preact diffs against existing children,
15
+ // so the spinner must be gone before the first render.
16
+ root.textContent = '';
14
17
  render(html`<${App}/>`, root);
15
18
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * AgentCard — card, avatar, chips, and detail drawer for the Agents view.
3
+ *
4
+ * Extracted from AgentsView so the view module stays focused on grouping,
5
+ * search, and fetch state. Per-role accent colors are driven by a single
6
+ * `agent-accent--<type>` class on the card/drawer root: it sets the
7
+ * --agent-accent custom property that the avatar, role badge, and hover
8
+ * border all read (see the AGENTS VIEW block at the end of css.js).
9
+ */
10
+
11
+ import { html } from '../preact.js';
12
+ import { setState } from '../store.js';
13
+ import { pressable, showToast } from './shared.js';
14
+ import { renderMd } from '../util.js';
15
+
16
+ const MAX_CARD_TOOL_CHIPS = 4;
17
+
18
+ /** "Sadiq Damani" -> "SD", "Hussain" -> "H". */
19
+ function initialsOf(name) {
20
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map(w => w[0]).join('').toUpperCase();
21
+ }
22
+
23
+ /** Per-role accent class — types map 1:1 to the CSS accent variants. */
24
+ export function accentClass(agent) {
25
+ return 'agent-accent--' + (agent.type || 'system');
26
+ }
27
+
28
+ // ---- Avatar circle with initials ----
29
+ function Avatar({ agent, large }) {
30
+ return html`<span class=${'agent-avatar' + (large ? ' agent-avatar--lg' : '')} aria-hidden="true">${initialsOf(agent.name)}</span>`;
31
+ }
32
+
33
+ // ---- Metadata chips (model + tools), shared by card and drawer ----
34
+ export function MetaChips({ meta, maxTools }) {
35
+ if (!meta) return null;
36
+ const tools = meta.tools || [];
37
+ const shown = maxTools ? tools.slice(0, maxTools) : tools;
38
+ const extra = tools.length - shown.length;
39
+ if (!meta.model && !shown.length) return null;
40
+ return html`
41
+ <div class="agent-chips">
42
+ ${meta.model ? html`<span class="agent-chip agent-chip--model">${meta.model}</span>` : null}
43
+ ${shown.map(t => html`<span class="agent-chip" key=${t}>${t}</span>`)}
44
+ ${extra > 0 ? html`<span class="agent-chip agent-chip--more">+${extra}</span>` : null}
45
+ </div>
46
+ `;
47
+ }
48
+
49
+ // ---- Single agent card ----
50
+ export function AgentCard({ agent, meta, onOpen }) {
51
+ return html`
52
+ <div class=${'agent-card ' + accentClass(agent)} ...${pressable(() => onOpen(agent))}>
53
+ <div class="agent-card-top">
54
+ <${Avatar} agent=${agent} />
55
+ <div class="agent-card-id">
56
+ <div class="agent-card-name">
57
+ ${agent.name}
58
+ ${agent.real ? html`<span class="real-badge">real</span>` : null}
59
+ </div>
60
+ <span class="role-badge">${agent.role}</span>
61
+ </div>
62
+ <span class="agent-card-arabic">${agent.arabic}</span>
63
+ </div>
64
+ ${meta && meta.description ? html`<p class="agent-card-desc">${meta.description}</p>` : null}
65
+ <${MetaChips} meta=${meta} maxTools=${MAX_CARD_TOOL_CHIPS} />
66
+ </div>
67
+ `;
68
+ }
69
+
70
+ // ---- Detail drawer ----
71
+ export function AgentDrawer({ agent, meta, prompt, onClose }) {
72
+ const filePath = agent.file ? 'rcode/agents/' + agent.file : null;
73
+
74
+ function copyPath() {
75
+ navigator.clipboard.writeText(filePath).then(() => {
76
+ showToast('Path copied!');
77
+ }).catch(() => {});
78
+ }
79
+
80
+ function openInFiles() {
81
+ setState({ requestedFile: filePath });
82
+ window.location.hash = 'files';
83
+ }
84
+
85
+ let body;
86
+ if (!agent.file) {
87
+ body = html`<div class="agent-drawer-empty">No prompt file on disk — this is a system entry without an agent definition.</div>`;
88
+ } else if (prompt.loading) {
89
+ body = html`
90
+ <div class="skeleton"></div>
91
+ <div class="agent-drawer-skeleton skeleton"></div>
92
+ `;
93
+ } else if (prompt.error) {
94
+ body = html`<div class="agent-drawer-error">${prompt.error}</div>`;
95
+ } else if (prompt.text) {
96
+ body = html`<div class="md-render" dangerouslySetInnerHTML=${{ __html: renderMd(prompt.text) }} />`;
97
+ } else {
98
+ body = null;
99
+ }
100
+
101
+ return html`
102
+ <div class="agent-drawer-backdrop" onClick=${onClose}></div>
103
+ <aside class=${'agent-drawer ' + accentClass(agent)} role="dialog" aria-modal="true" aria-label="${agent.name} — full prompt">
104
+ <div class="agent-drawer-head">
105
+ <${Avatar} agent=${agent} large />
106
+ <div class="agent-drawer-titles">
107
+ <div class="agent-drawer-name">
108
+ ${agent.name}
109
+ <span class="agent-drawer-arabic">${agent.arabic}</span>
110
+ ${agent.real ? html`<span class="real-badge">real</span>` : null}
111
+ </div>
112
+ <span class="role-badge">${agent.role}</span>
113
+ <${MetaChips} meta=${meta} />
114
+ </div>
115
+ <button class="agent-drawer-close" onClick=${onClose} aria-label="Close">×</button>
116
+ </div>
117
+ ${filePath ? html`
118
+ <div class="agent-drawer-meta">
119
+ <span class="agent-drawer-meta-path">${filePath}</span>
120
+ <button class="agent-drawer-btn" onClick=${copyPath}>Copy path</button>
121
+ <button class="agent-drawer-btn agent-drawer-btn--link" onClick=${openInFiles}>View in Files →</button>
122
+ </div>
123
+ ` : null}
124
+ <div class="agent-drawer-body">${body}</div>
125
+ </aside>
126
+ `;
127
+ }