@hanzlaa/rcode 3.4.33 → 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 (106) hide show
  1. package/AGENTS.md +6 -6
  2. package/CONTRIBUTING.md +2 -0
  3. package/LICENSE +21 -0
  4. package/README.md +66 -403
  5. package/cli/doctor.js +87 -1
  6. package/cli/install.js +122 -31
  7. package/cli/lib/schemas.cjs +318 -0
  8. package/cli/postinstall.js +19 -3
  9. package/dist/rcode.js +316 -23
  10. package/package.json +14 -4
  11. package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
  12. package/rihal/agents/rihal-dep-auditor.md +1 -1
  13. package/rihal/agents/rihal-docs-auditor.md +3 -145
  14. package/rihal/agents/rihal-i18n-auditor.md +1 -1
  15. package/rihal/agents/rihal-nyquist-auditor.md +4 -156
  16. package/rihal/agents/rihal-observability-auditor.md +1 -1
  17. package/rihal/bin/rihal-hooks.cjs +394 -4
  18. package/rihal/bin/rihal-tools.cjs +891 -24
  19. package/rihal/commands/create-prd.md +18 -0
  20. package/rihal/commands/execute-milestone.md +18 -0
  21. package/rihal/commands/plan-milestone.md +18 -0
  22. package/rihal/commands/scaffold-milestone.md +18 -0
  23. package/rihal/commands/scaffold-skill.md +18 -0
  24. package/rihal/references/REFERENCES_INDEX.md +49 -7
  25. package/rihal/references/agent-contracts.md +10 -0
  26. package/rihal/references/design-tokens.md +98 -0
  27. package/rihal/references/docs-auditor-playbook.md +148 -0
  28. package/rihal/references/git-preflight.md +117 -0
  29. package/rihal/references/iterative-retrieval.md +85 -0
  30. package/rihal/references/nyquist-auditor-playbook.md +157 -0
  31. package/rihal/references/workstream-flag.md +2 -2
  32. package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
  33. package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
  34. package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
  35. package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +2 -2
  36. package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
  37. package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
  38. package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
  39. package/rihal/templates/settings-hooks.json +39 -0
  40. package/rihal/workflows/check-todos.md +4 -0
  41. package/rihal/workflows/code-review-fix.md +4 -3
  42. package/rihal/workflows/code-review.md +1 -1
  43. package/rihal/workflows/debug.md +1 -1
  44. package/rihal/workflows/dev-story.md +4 -0
  45. package/rihal/workflows/diff.md +2 -2
  46. package/rihal/workflows/do.md +16 -8
  47. package/rihal/workflows/docs-update.md +2 -2
  48. package/rihal/workflows/enable-hooks.md +6 -1
  49. package/rihal/workflows/execute-milestone.md +139 -0
  50. package/rihal/workflows/execute-regression-gates.md +1 -1
  51. package/rihal/workflows/execute-sprint.md +54 -2
  52. package/rihal/workflows/execute-verify-phase-goal.md +31 -4
  53. package/rihal/workflows/execute-waves.md +33 -5
  54. package/rihal/workflows/execute.md +40 -6
  55. package/rihal/workflows/help.md +1 -1
  56. package/rihal/workflows/import.md +1 -1
  57. package/rihal/workflows/lens-audit.md +39 -23
  58. package/rihal/workflows/list-workspaces.md +1 -1
  59. package/rihal/workflows/map-codebase.md +4 -4
  60. package/rihal/workflows/new-milestone.md +18 -1
  61. package/rihal/workflows/new-project-research.md +53 -1
  62. package/rihal/workflows/new-workspace.md +1 -1
  63. package/rihal/workflows/plan-milestone.md +105 -0
  64. package/rihal/workflows/plan-research-validation.md +1 -1
  65. package/rihal/workflows/plan-spawn-planner.md +1 -1
  66. package/rihal/workflows/plan.md +31 -3
  67. package/rihal/workflows/plant-seed.md +6 -0
  68. package/rihal/workflows/quick.md +11 -5
  69. package/rihal/workflows/research-phase.md +24 -0
  70. package/rihal/workflows/scaffold-milestone.md +60 -0
  71. package/rihal/workflows/scaffold-skill.md +137 -0
  72. package/rihal/workflows/scan.md +1 -1
  73. package/rihal/workflows/session-report.md +43 -3
  74. package/rihal/workflows/verify-work.md +3 -3
  75. package/server/dashboard.js +154 -5
  76. package/server/lib/html/client/agents-data.js +27 -0
  77. package/server/lib/html/client/app.js +15 -0
  78. package/server/lib/html/client/components/App.js +211 -0
  79. package/server/lib/html/client/components/OrchPanel.js +293 -0
  80. package/server/lib/html/client/components/Sidebar.js +73 -0
  81. package/server/lib/html/client/components/Topbar.js +53 -0
  82. package/server/lib/html/client/components/XtermPanel.js +220 -0
  83. package/server/lib/html/client/components/shared.js +330 -0
  84. package/server/lib/html/client/icons-client.js +85 -0
  85. package/server/lib/html/client/orchestrator.js +279 -0
  86. package/server/lib/html/client/preact.js +34 -0
  87. package/server/lib/html/client/store.js +91 -0
  88. package/server/lib/html/client/util.js +186 -0
  89. package/server/lib/html/client/views/AgentsView.js +83 -0
  90. package/server/lib/html/client/views/DecisionsView.js +102 -0
  91. package/server/lib/html/client/views/FilesView.js +223 -0
  92. package/server/lib/html/client/views/KanbanView.js +236 -0
  93. package/server/lib/html/client/views/MemoryView.js +157 -0
  94. package/server/lib/html/client/views/MilestonesView.js +136 -0
  95. package/server/lib/html/client/views/OrchestrationView.js +167 -0
  96. package/server/lib/html/client/views/OverviewView.js +221 -0
  97. package/server/lib/html/client/views/PhasesView.js +184 -0
  98. package/server/lib/html/client/views/RoadmapView.js +238 -0
  99. package/server/lib/html/client/views/SprintsView.js +178 -0
  100. package/server/lib/html/client/views/TasksView.js +148 -0
  101. package/server/lib/html/client.js +42 -1064
  102. package/server/lib/html/css.js +2266 -466
  103. package/server/lib/html/icons.js +68 -0
  104. package/server/lib/html/shell.js +16 -210
  105. package/server/lib/scanner.js +109 -0
  106. package/server/orchestrator.js +362 -0
@@ -75,7 +75,40 @@ Extract counts:
75
75
  - **Chains:** count of `*-chain.md` files
76
76
  - **Discusses:** count of `*-discuss.md` files
77
77
 
78
- ## Step 5 — Estimate token usage
78
+ ## Step 5 — Token usage (measured or estimated)
79
+
80
+ ### Step 5a — Prefer measured totals from cost.jsonl
81
+
82
+ If the `cost-track` hook (#745) is enabled, it appends one usage record per
83
+ response to `.rihal/telemetry/cost.jsonl`. Check for it first:
84
+
85
+ ```bash
86
+ test -f .rihal/telemetry/cost.jsonl && echo "measured" || echo "estimated"
87
+ ```
88
+
89
+ **If `.rihal/telemetry/cost.jsonl` exists:** sum the `input_tokens` and
90
+ `output_tokens` across every line and report the **measured** totals — label
91
+ them clearly as "measured":
92
+
93
+ ```bash
94
+ node -e "
95
+ const fs=require('fs');
96
+ let i=0,o=0,n=0;
97
+ for(const l of fs.readFileSync('.rihal/telemetry/cost.jsonl','utf8').split('\n').filter(Boolean)){
98
+ try{const r=JSON.parse(l);i+=r.input_tokens||0;o+=r.output_tokens||0;n++;}catch{}
99
+ }
100
+ console.log('responses='+n,'input='+i,'output='+o,'total='+(i+o));
101
+ "
102
+ ```
103
+
104
+ Report: `Total (measured): {input} input + {output} output = {total} tokens
105
+ across {responses} responses`. Skip the heuristic estimate below — measured
106
+ data supersedes it.
107
+
108
+ **If `.rihal/telemetry/cost.jsonl` does NOT exist:** fall back to the heuristic
109
+ estimate in Step 5b and label the result "estimated".
110
+
111
+ ### Step 5b — Heuristic estimate (fallback only)
79
112
 
80
113
  Calculate estimated tokens (note: these are rough approximations, not actual measurements).
81
114
 
@@ -135,9 +168,16 @@ Write `.planning/SESSION-REPORT-{YYYY-MM-DD-HHmmss}.md` with this structure:
135
168
  - **Blockers Identified:** {count} ({open_count} open)
136
169
  - **Commits:** {count}
137
170
 
138
- ## Estimated Token Usage
171
+ ## Token Usage ({measured|estimated})
172
+
173
+ **If cost.jsonl exists — measured:**
174
+
175
+ - Responses tracked: {count}
176
+ - Input tokens: {input}
177
+ - Output tokens: {output}
178
+ - **Total (measured): {grand_total} tokens**
139
179
 
140
- **Note:** Token estimates are approximations based on artifact counts.
180
+ **If cost.jsonl absent estimated** (approximations based on artifact counts):
141
181
 
142
182
  - Council Sessions: {count} × 50K = {total}K
143
183
  - Chains: {count} × 30K = {total}K
@@ -523,7 +523,7 @@ Plans must be executable prompts.
523
523
  </downstream_consumer>
524
524
  """,
525
525
  subagent_type="rihal-planner",
526
- model="sonnet",
526
+ model="{model}",
527
527
  model="{planner_model}",
528
528
  description="Plan gap fixes for Phase {phase}"
529
529
  )
@@ -573,7 +573,7 @@ Return one of:
573
573
  </expected_output>
574
574
  """,
575
575
  subagent_type="rihal-sprint-checker",
576
- model="sonnet",
576
+ model="{model}",
577
577
  model="{checker_model}",
578
578
  description="Verify Phase {phase} fix plans"
579
579
  )
@@ -618,7 +618,7 @@ Do NOT replan from scratch unless issues are fundamental.
618
618
  </instructions>
619
619
  """,
620
620
  subagent_type="rihal-planner",
621
- model="sonnet",
621
+ model="{model}",
622
622
  model="{planner_model}",
623
623
  description="Revise Phase {phase} plans"
624
624
  )
@@ -20,8 +20,15 @@
20
20
  * Stop: kill $(lsof -t -i:7717)
21
21
  */
22
22
 
23
- const http = require('http');
24
- const path = require('path');
23
+ const http = require('http');
24
+ const path = require('path');
25
+ const fs = require('fs');
26
+ const os = require('os');
27
+ const crypto = require('crypto');
28
+ const { spawn } = require('child_process');
29
+
30
+ // Client JS modules live here and are served verbatim at /js/<name>.js
31
+ const CLIENT_DIR = path.join(__dirname, 'lib', 'html', 'client');
25
32
 
26
33
  const { scanState } = require('./lib/scanner');
27
34
  const { handleApiState, handleApiFiles, handleApiFile, handleApiHierarchy, handleApiMemory } = require('./lib/api');
@@ -32,6 +39,26 @@ const PORT = parseInt(process.env.PORT || '7717', 10);
32
39
  const RIHAL_DIR = process.env.RIHAL_DIR || path.join(process.cwd(), '.rihal');
33
40
  const PROJECT_ROOT = path.dirname(RIHAL_DIR);
34
41
 
42
+ // Shared orchestrator token — passed to the orchestrator via env and embedded
43
+ // in the HTML. Persisted to ~/.rihal/orch-token so it stays STABLE across
44
+ // dashboard restarts; otherwise every restart invalidates the token baked
45
+ // into already-open browser tabs and their API calls 401.
46
+ function loadOrchToken() {
47
+ if (process.env.ORCH_TOKEN) return process.env.ORCH_TOKEN;
48
+ const tokenFile = path.join(os.homedir(), '.rihal', 'orch-token');
49
+ try {
50
+ const existing = fs.readFileSync(tokenFile, 'utf8').trim();
51
+ if (existing) return existing;
52
+ } catch { /* not yet created */ }
53
+ const token = crypto.randomBytes(24).toString('hex');
54
+ try {
55
+ fs.mkdirSync(path.dirname(tokenFile), { recursive: true });
56
+ fs.writeFileSync(tokenFile, token, { mode: 0o600 });
57
+ } catch { /* non-fatal — fall back to an in-memory token */ }
58
+ return token;
59
+ }
60
+ const ORCH_TOKEN = loadOrchToken();
61
+
35
62
  // ---------- HTTP Server ----------
36
63
  const server = http.createServer((req, res) => {
37
64
  const url = req.url || '/';
@@ -67,9 +94,40 @@ const server = http.createServer((req, res) => {
67
94
  return;
68
95
  }
69
96
 
97
+ // Lets the client fetch the current orchestrator token at runtime, so a
98
+ // long-open tab can self-heal instead of 401'ing if the token ever drifts.
99
+ if (url === '/api/orch-token') {
100
+ res.writeHead(200, { 'Content-Type': 'application/json' });
101
+ res.end(JSON.stringify({ token: ORCH_TOKEN }));
102
+ return;
103
+ }
104
+
105
+ if (url.startsWith('/js/')) {
106
+ 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; }
111
+ // Defense-in-depth: resolved path must stay inside CLIENT_DIR even after
112
+ // any OS-level resolution (handles encoded traversal the regex might miss).
113
+ const resolved = path.resolve(CLIENT_DIR, name);
114
+ if (!resolved.startsWith(CLIENT_DIR + path.sep) && resolved !== CLIENT_DIR) {
115
+ res.writeHead(403); res.end('Forbidden'); return;
116
+ }
117
+ fs.readFile(resolved, (err, data) => {
118
+ if (err) { res.writeHead(404); res.end('Not found'); return; }
119
+ res.writeHead(200, {
120
+ 'Content-Type': 'application/javascript; charset=utf-8',
121
+ 'Cache-Control': 'no-cache',
122
+ });
123
+ res.end(data);
124
+ });
125
+ return;
126
+ }
127
+
70
128
  if (url === '/' || url === '/index.html') {
71
129
  const state = scanState(RIHAL_DIR);
72
- const html = renderHtml(state);
130
+ const html = renderHtml(state, ORCH_TOKEN);
73
131
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
74
132
  res.end(html);
75
133
  return;
@@ -91,6 +149,97 @@ server.listen(PORT, '127.0.0.1', () => {
91
149
  console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
92
150
  });
93
151
 
152
+ // ── Ensure interactive-terminal native module is present ─────────
153
+ // @lydell/node-pty is an optionalDependency, so it can be absent if the
154
+ // package was installed with --omit=optional or a partial CI install.
155
+ // It ships prebuilt binaries (no node-gyp), so fetching it is a fast,
156
+ // no-compile, one-time step. Runs async — the dashboard never blocks; the
157
+ // orchestrator is spawned via the callback once the install settles.
158
+ // Failure is non-fatal: the terminal just degrades with a clear message.
159
+ function ensurePty(done) {
160
+ try { require.resolve('@lydell/node-pty'); done(); return; } catch {}
161
+
162
+ const pkgRoot = path.join(__dirname, '..');
163
+
164
+ // @lydell/node-pty is already declared in optionalDependencies, so a plain
165
+ // lockfile-respecting `install` pulls it in without mutating package.json.
166
+ // Use pnpm when the repo is pnpm-managed — `npm install` fights pnpm's
167
+ // symlinked node_modules and stalls. End-user installs use npm.
168
+ const usePnpm = fs.existsSync(path.join(pkgRoot, 'pnpm-lock.yaml'));
169
+ const cmd = usePnpm ? 'pnpm' : 'npm';
170
+ const args = usePnpm
171
+ ? ['install', '--ignore-scripts']
172
+ : ['install', '--ignore-scripts', '--no-audit', '--no-fund'];
173
+
174
+ console.log('[setup] Installing interactive-terminal support (@lydell/node-pty)…');
175
+ let settled = false;
176
+ const finish = (ok) => {
177
+ if (settled) return;
178
+ settled = true;
179
+ console.log(ok ? '[setup] Interactive terminal ready.'
180
+ : '[setup] node-pty install incomplete — terminal stays unavailable.');
181
+ done();
182
+ };
183
+
184
+ let child;
185
+ try {
186
+ child = spawn(cmd, args, {
187
+ cwd: pkgRoot, stdio: 'inherit', shell: process.platform === 'win32',
188
+ });
189
+ } catch (err) {
190
+ console.log('[setup] node-pty install could not start:', err.message);
191
+ finish(false);
192
+ return;
193
+ }
194
+ const timer = setTimeout(() => { try { child.kill(); } catch {} }, 180000);
195
+ child.on('exit', code => { clearTimeout(timer); finish(code === 0); });
196
+ child.on('error', err => { clearTimeout(timer);
197
+ console.log('[setup] node-pty install error:', err.message); finish(false); });
198
+ }
199
+
200
+ // ── Auto-spawn orchestrator (port 7718) ──────────────────────────
201
+ const ORCH_BIN = path.join(__dirname, 'orchestrator.js');
202
+ let _orchProc = null;
203
+
204
+ function spawnOrchestrator() {
205
+ try {
206
+ _orchProc = spawn(process.execPath, [ORCH_BIN], {
207
+ cwd: path.join(__dirname, '..'),
208
+ env: { ...process.env, ORCH_TOKEN },
209
+ stdio: 'pipe',
210
+ });
211
+ _orchProc.stdout.on('data', chunk => {
212
+ const msg = chunk.toString().trim();
213
+ if (msg) console.log('[orch]', msg);
214
+ });
215
+ _orchProc.stderr.on('data', chunk => {
216
+ const msg = chunk.toString().trim();
217
+ if (msg && !msg.includes('no stdin')) console.error('[orch]', msg);
218
+ });
219
+ _orchProc.on('exit', (code, signal) => {
220
+ _orchProc = null;
221
+ if (signal !== 'SIGTERM' && signal !== 'SIGINT') {
222
+ console.log(`[orch] exited (${code}) — restarting in 3s…`);
223
+ setTimeout(spawnOrchestrator, 3000);
224
+ }
225
+ });
226
+ _orchProc.on('error', err => {
227
+ console.error('[orch] spawn error:', err.message);
228
+ _orchProc = null;
229
+ });
230
+ console.log('[orch] orchestrator started (port 7718)');
231
+ } catch (err) {
232
+ console.error('[orch] failed to start:', err.message);
233
+ }
234
+ }
235
+
236
+ // Orchestrator spawns only once node-pty is settled (present or installed).
237
+ ensurePty(spawnOrchestrator);
238
+
94
239
  // Graceful shutdown
95
- process.on('SIGTERM', () => server.close(() => process.exit(0)));
96
- process.on('SIGINT', () => server.close(() => process.exit(0)));
240
+ function shutdown() {
241
+ if (_orchProc) { try { _orchProc.kill('SIGTERM'); } catch {} }
242
+ server.close(() => process.exit(0));
243
+ }
244
+ process.on('SIGTERM', shutdown);
245
+ process.on('SIGINT', shutdown);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * agents-data.js — the 18-agent roster, moved client-side from shell.js.
3
+ *
4
+ * Previously lived in shell.js:17-36 as a server-rendered array.
5
+ * Now exported as a pure ESM constant so AgentsView can render it.
6
+ */
7
+
8
+ 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' },
27
+ ];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ESM entry point — mounts the Preact App into #app-root.
3
+ *
4
+ * Loaded via <script type="module" src="/js/app.js"> from client.js,
5
+ * AFTER the legacy <script src> modules (which fill the 10 un-migrated
6
+ * view host divs). Legacy modules remain active during coexistence phase.
7
+ */
8
+
9
+ import { render, html } from './preact.js';
10
+ import { App } from './components/App.js';
11
+
12
+ const root = document.getElementById('app-root');
13
+ if (root) {
14
+ render(html`<${App}/>`, root);
15
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * App — root Preact component.
3
+ *
4
+ * Owns:
5
+ * - Hash router (view + subId state, hashchange listener)
6
+ * - Layout: Sidebar + content area + Topbar + all 12 Preact view components
7
+ * - 30s auto-refresh: polls /api/state, diffs lastScanned, calls setState
8
+ * - Theme toggle: reads/persists localStorage('majlis-theme')
9
+ *
10
+ * Sprint 31.4 completed the Preact migration. All 12 views are Preact
11
+ * components. Legacy client-main.js, client-render.js, and client-kanban.js
12
+ * are deleted. No coexistence seam remains.
13
+ */
14
+
15
+ import { html, useState, useEffect, useRef, useCallback } from '../preact.js';
16
+ import { getState, setState, subscribe } from '../store.js';
17
+ import { startSessionsPoll, refreshOrchToken } from '../orchestrator.js';
18
+ import { Sidebar } from './Sidebar.js';
19
+ import { Topbar } from './Topbar.js';
20
+ import { XtermPanel } from './XtermPanel.js';
21
+ import { OrchPanel } from './OrchPanel.js';
22
+ import { OverviewView } from '../views/OverviewView.js';
23
+ import { DecisionsView } from '../views/DecisionsView.js';
24
+ import { RoadmapView } from '../views/RoadmapView.js';
25
+ import { MilestonesView } from '../views/MilestonesView.js';
26
+ import { PhasesView } from '../views/PhasesView.js';
27
+ import { SprintsView } from '../views/SprintsView.js';
28
+ import { TasksView } from '../views/TasksView.js';
29
+ import { KanbanView } from '../views/KanbanView.js';
30
+ import { FilesView } from '../views/FilesView.js';
31
+ import { AgentsView } from '../views/AgentsView.js';
32
+ import { MemoryView } from '../views/MemoryView.js';
33
+ import { OrchestrationView } from '../views/OrchestrationView.js';
34
+
35
+ // Views served by Preact components (migrated)
36
+ // Sprint 31.4: +orchestration → all 12 views Preact. Migration complete.
37
+ const PREACT_VIEWS = {
38
+ overview: OverviewView,
39
+ decisions: DecisionsView,
40
+ roadmap: RoadmapView,
41
+ milestones: MilestonesView,
42
+ phases: PhasesView,
43
+ sprints: SprintsView,
44
+ tasks: TasksView,
45
+ kanban: KanbanView,
46
+ files: FilesView,
47
+ agents: AgentsView,
48
+ memory: MemoryView,
49
+ orchestration: OrchestrationView,
50
+ };
51
+
52
+ // All views are now Preact — no legacy placeholder hosts needed.
53
+ const LEGACY_VIEWS = [];
54
+
55
+ const ALL_VIEWS = Object.keys(PREACT_VIEWS).concat(LEGACY_VIEWS);
56
+
57
+ /** Parse location.hash into { view, subId } — port of client-main.js:45-49. */
58
+ function parseHash() {
59
+ const raw = location.hash.slice(1) || 'overview';
60
+ const slash = raw.indexOf('/');
61
+ const view = slash === -1 ? raw : raw.slice(0, slash);
62
+ const subId = slash === -1 ? null : raw.slice(slash + 1);
63
+ // #263: unknown hash falls back to overview
64
+ const resolvedView = ALL_VIEWS.includes(view) ? view : 'overview';
65
+ return { view: resolvedView, subId };
66
+ }
67
+
68
+ /** Root App component. No props needed — reads everything from the store. */
69
+ export function App() {
70
+ // ---- Router state ----
71
+ const [{ view, subId }, setRoute] = useState(parseHash);
72
+
73
+ useEffect(() => {
74
+ function onHashChange() { setRoute(parseHash()); }
75
+ window.addEventListener('hashchange', onHashChange);
76
+ return () => window.removeEventListener('hashchange', onHashChange);
77
+ }, []);
78
+
79
+ // ---- Store state (for projectName and pass-through to views) ----
80
+ const [storeState, setStoreState] = useState(getState);
81
+ useEffect(() => {
82
+ const unsub = subscribe(newState => setStoreState({ ...newState }));
83
+ return unsub;
84
+ }, []);
85
+
86
+ // ---- Theme ----
87
+ const [theme, setTheme] = useState(() => {
88
+ const saved = localStorage.getItem('majlis-theme') || 'dark';
89
+ if (saved === 'light') {
90
+ document.documentElement.setAttribute('data-theme', 'light');
91
+ }
92
+ return saved;
93
+ });
94
+
95
+ const toggleTheme = useCallback(() => {
96
+ const next = theme === 'light' ? 'dark' : 'light';
97
+ document.documentElement.setAttribute('data-theme', next === 'dark' ? '' : next);
98
+ localStorage.setItem('majlis-theme', next);
99
+ setTheme(next);
100
+ }, [theme]);
101
+
102
+ // ---- Sidebar collapse ----
103
+ const toggleSidebar = useCallback(() => {
104
+ const sidebar = document.querySelector('.sidebar');
105
+ const backdrop = document.getElementById('sidebar-backdrop');
106
+ if (!sidebar) return;
107
+ const open = sidebar.classList.toggle('sidebar-open');
108
+ if (backdrop) backdrop.classList.toggle('active', open);
109
+ document.body.classList.toggle('sidebar-visible', open);
110
+ }, []);
111
+
112
+ // ---- Updated-ago display ----
113
+ const [updatedAgo, setUpdatedAgo] = useState('just now');
114
+ const scanTimeRef = useRef(Date.now());
115
+
116
+ useEffect(() => {
117
+ const id = setInterval(() => {
118
+ const s = Math.floor((Date.now() - scanTimeRef.current) / 1000);
119
+ setUpdatedAgo(s < 5 ? 'just now' : s < 60 ? s + 's ago' : Math.floor(s / 60) + 'm ago');
120
+ }, 1000);
121
+ return () => clearInterval(id);
122
+ }, []);
123
+
124
+ // ---- Manual refresh ----
125
+ const lastScannedRef = useRef(null);
126
+
127
+ const fetchAndRerender = useCallback(async () => {
128
+ const btn = document.getElementById('refresh-btn');
129
+ if (btn) btn.textContent = '↺ …';
130
+ try {
131
+ const r = await fetch('/api/state');
132
+ if (!r.ok) return;
133
+ const newState = await r.json();
134
+ lastScannedRef.current = newState.lastScanned;
135
+ scanTimeRef.current = Date.now();
136
+ setUpdatedAgo('just now');
137
+ if (newState.raw) {
138
+ setState({
139
+ phases: newState.phaseTree || newState.raw.phases || [],
140
+ milestone: newState.raw.milestone || '',
141
+ currentPhase: newState.raw.current_phase || null,
142
+ currentSprint: newState.raw.current_sprint || null,
143
+ decisions: newState.raw.decisions || [],
144
+ blockers: newState.raw.blockers || [],
145
+ council_sessions: newState.raw.council_sessions || [],
146
+ last_session: newState.raw.last_session || null,
147
+ });
148
+ }
149
+ } catch { /* network errors ignored */ }
150
+ if (btn) btn.textContent = '↺ Refresh';
151
+ }, []);
152
+
153
+ // ---- 30s auto-refresh ----
154
+ useEffect(() => {
155
+ const id = setInterval(async () => {
156
+ try {
157
+ const r = await fetch('/api/state');
158
+ if (!r.ok) return;
159
+ const s = await r.json();
160
+ if (s.lastScanned !== lastScannedRef.current) await fetchAndRerender();
161
+ } catch { /* ignore */ }
162
+ }, 30000);
163
+ return () => clearInterval(id);
164
+ }, [fetchAndRerender]);
165
+
166
+ // Expose manualRefresh globally for any legacy onclick="manualRefresh()" callers
167
+ useEffect(() => {
168
+ window._preactRefresh = fetchAndRerender;
169
+ }, [fetchAndRerender]);
170
+
171
+ // Start the global session poll and refresh the orchestrator token on boot.
172
+ useEffect(() => {
173
+ refreshOrchToken();
174
+ startSessionsPoll();
175
+ }, []);
176
+
177
+ // ---- View rendering ----
178
+ const PreactView = PREACT_VIEWS[view] || null;
179
+
180
+ return html`
181
+ <div class="app-shell">
182
+ <${Sidebar} activeView=${view} projectName=${storeState.projectName || ''} />
183
+
184
+ <div id="sidebar-backdrop" onClick=${() => {
185
+ const sidebar = document.querySelector('.sidebar');
186
+ const backdrop = document.getElementById('sidebar-backdrop');
187
+ if (sidebar) sidebar.classList.remove('sidebar-open');
188
+ if (backdrop) backdrop.classList.remove('active');
189
+ document.body.classList.remove('sidebar-visible');
190
+ }}></div>
191
+
192
+ <div class="content-area" id="main-content">
193
+ <${Topbar}
194
+ projectName=${storeState.projectName || ''}
195
+ updatedAgo=${updatedAgo}
196
+ onRefresh=${fetchAndRerender}
197
+ onToggleTheme=${toggleTheme}
198
+ onToggleSidebar=${toggleSidebar}
199
+ themeLabel=${theme}
200
+ />
201
+
202
+ <div class="main-scroll" id="main-scroll">
203
+ ${PreactView ? html`<${PreactView} subId=${subId} />` : null}
204
+ </div>
205
+ </div>
206
+
207
+ <${XtermPanel} />
208
+ <${OrchPanel} />
209
+ </div>
210
+ `;
211
+ }