@hanzlaa/rcode 4.5.0 → 4.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.
- package/package.json +1 -1
- package/rcode/agents/rcode-mariam.md +6 -0
- package/rcode/agents/rcode-sadiq.md +6 -0
- package/rcode/agents/rcode-waleed.md +6 -0
- package/rcode/bin/rcode-hooks.cjs +41 -5
- package/rcode/skills/agents/mariam-marketing/SKILL.md +1 -0
- package/rcode/skills/agents/sadiq-analyst/SKILL.md +1 -0
- package/rcode/skills/agents/waleed-architect/SKILL.md +1 -0
- package/rcode/workflows/council.md +29 -1
- package/server/dashboard.js +6 -3
- package/server/lib/html/client/components/App.js +1 -1
- package/server/lib/html/client/components/OrchPanel.js +2 -2
- package/server/lib/html/client/components/XtermPanel.js +98 -23
- package/server/lib/html/client/orchestrator.js +28 -15
- package/server/lib/html/client/views/OrchestrationView.js +247 -169
- package/server/lib/html/css.js +596 -227
- package/server/lib/html/shell.js +9 -3
- package/server/orchestrator.js +3 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
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": {
|
|
@@ -16,3 +16,9 @@ color: purple
|
|
|
16
16
|
@.rcode/references/agent-shared-rules.md
|
|
17
17
|
@.rcode/references/codebase-grounding.md
|
|
18
18
|
@.rcode/skills/agents/mariam-marketing/SKILL.md
|
|
19
|
+
|
|
20
|
+
## Grounding rule (mandatory)
|
|
21
|
+
|
|
22
|
+
Any pricing, fee, rate, market-size, or regulation claim MUST be verified with
|
|
23
|
+
WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
24
|
+
Do not present training-data numbers as current fact.
|
|
@@ -16,3 +16,9 @@ color: blue
|
|
|
16
16
|
@.rcode/references/agent-shared-rules.md
|
|
17
17
|
@.rcode/references/codebase-grounding.md
|
|
18
18
|
@.rcode/skills/agents/sadiq-analyst/SKILL.md
|
|
19
|
+
|
|
20
|
+
## Grounding rule (mandatory)
|
|
21
|
+
|
|
22
|
+
Any pricing, fee, rate, market-size, or regulation claim MUST be verified with
|
|
23
|
+
WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
24
|
+
Do not present training-data numbers as current fact.
|
|
@@ -18,3 +18,9 @@ color: green
|
|
|
18
18
|
@.rcode/references/codebase-grounding.md
|
|
19
19
|
@.rcode/references/karpathy-guidelines.md
|
|
20
20
|
@.rcode/skills/agents/waleed-architect/SKILL.md
|
|
21
|
+
|
|
22
|
+
## Grounding rule (mandatory)
|
|
23
|
+
|
|
24
|
+
Any pricing, fee, rate, market-size, or regulation claim MUST be verified with
|
|
25
|
+
WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
26
|
+
Do not present training-data numbers as current fact.
|
|
@@ -25,8 +25,43 @@ const fs = require('fs');
|
|
|
25
25
|
const os = require('os');
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const { execSync, spawnSync } = require('child_process');
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Self-healing lib require (#960). When this file runs from an installed
|
|
31
|
+
* `.rcode/bin/` whose `lib/` is stale or partial (fresh git worktree, merge/
|
|
32
|
+
* pull that changed `rcode/bin/lib/` without a mirror sync), a hard require
|
|
33
|
+
* crashes EVERY hook — the user sees a SessionStart loader error and loses
|
|
34
|
+
* the status line entirely. Instead: on MODULE_NOT_FOUND, try healing from
|
|
35
|
+
* the in-repo source of truth (`rcode/bin/lib/<name>` relative to the project
|
|
36
|
+
* root that contains this `.rcode/`), retry once, and otherwise fail open so
|
|
37
|
+
* hooks degrade (no memory injection / drift check) rather than die.
|
|
38
|
+
*/
|
|
39
|
+
function requireLib(name) {
|
|
40
|
+
const local = path.join(__dirname, 'lib', name);
|
|
41
|
+
try { return require(local); } catch (err) {
|
|
42
|
+
if (err && err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
43
|
+
try {
|
|
44
|
+
const src = path.join(__dirname, '..', '..', 'rcode', 'bin', 'lib', name);
|
|
45
|
+
if (fs.existsSync(src)) {
|
|
46
|
+
fs.mkdirSync(path.dirname(local), { recursive: true });
|
|
47
|
+
fs.copyFileSync(src, local);
|
|
48
|
+
return require(local);
|
|
49
|
+
}
|
|
50
|
+
} catch { /* healing is best-effort */ }
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const _stateReader = requireLib('state-reader.cjs') || {};
|
|
56
|
+
const resolveActivePhase = _stateReader.resolveActivePhase || (() => ({ activePhase: null, phaseLabel: null }));
|
|
57
|
+
const readSprintProgress = _stateReader.readSprintProgress || (() => ({ completedCount: 0, incompleteTasks: [] }));
|
|
58
|
+
const readRecentCommits = _stateReader.readRecentCommits || (() => []);
|
|
59
|
+
const readMilestoneHint = _stateReader.readMilestoneHint || (() => null);
|
|
60
|
+
|
|
61
|
+
const _memSelect = requireLib('memory-select.cjs') || {};
|
|
62
|
+
const selectMemoryChunks = _memSelect.selectMemoryChunks || (() => []);
|
|
63
|
+
const formatMemoryContext = _memSelect.formatMemoryContext || (() => '');
|
|
64
|
+
const hasMemory = _memSelect.hasMemory || (() => false);
|
|
30
65
|
|
|
31
66
|
// lib/memory-drift.cjs is optional at the module-load level: some hook-copy
|
|
32
67
|
// test fixtures deliberately stage a minimal bin/lib/ (only state-reader.cjs)
|
|
@@ -34,9 +69,10 @@ const { selectMemoryChunks, formatMemoryContext, hasMemory } = require('./lib/me
|
|
|
34
69
|
// require would crash every subcommand, not just `drift`/`post-commit`, so
|
|
35
70
|
// this loads lazily and fails open — same pattern as INTENT_TABLE below.
|
|
36
71
|
let checkDrift = null;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
{
|
|
73
|
+
const _drift = requireLib('memory-drift.cjs');
|
|
74
|
+
if (_drift) ({ checkDrift } = _drift);
|
|
75
|
+
}
|
|
40
76
|
|
|
41
77
|
/**
|
|
42
78
|
* Read and parse stdin JSON.
|
|
@@ -88,6 +88,7 @@ Five named heuristics. Cite by name when reasoning:
|
|
|
88
88
|
- **Never claim market readiness from < 4 disconfirmable signals.** Three customers is a focus group at best.
|
|
89
89
|
- **Never write a launch plan** without a 90-day proof point AND the kill criterion.
|
|
90
90
|
- **Never speculate on market data without WebSearch.** "unknown — would need 1 hour of research" is a valid answer.
|
|
91
|
+
- **Grounding rule (mandatory):** any pricing, fee, rate, market-size, or regulation claim MUST be verified with WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
91
92
|
- **Never write PRDs / user stories / architecture decisions.** Stay in the GTM lane.
|
|
92
93
|
- Brand consistency over clever campaigns
|
|
93
94
|
|
|
@@ -84,6 +84,7 @@ State the rule by name when refusing.
|
|
|
84
84
|
- **Never accept urgency manufactured by sales pressure** without independent market signal. Get the LOI in writing first.
|
|
85
85
|
- **Never make a strategic call under context-switch pressure.** If the user is tired or mid-fire, defer. Bad strategy at midnight is worse than no strategy.
|
|
86
86
|
- **Never write code, PRDs, or research reports.** Strategy directors set bets and kill switches; that's the deliverable.
|
|
87
|
+
- **Grounding rule (mandatory):** any pricing, fee, rate, market-size, or regulation claim MUST be verified with WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
87
88
|
|
|
88
89
|
## In Round 2 (council follow-ups)
|
|
89
90
|
|
|
@@ -86,6 +86,7 @@ State the rule by name when refusing.
|
|
|
86
86
|
- **Never propose "rewrite from scratch"** without a measurable pain point AND a parallel-run migration plan. Joel Spolsky test: if you can't write the migration plan in 200 words, the rewrite is wrong-shaped.
|
|
87
87
|
- **Never recommend bleeding-edge tech** for systems with multi-year lifetime expectations. Beta dependencies are a Reversibility-test fail.
|
|
88
88
|
- **Never write production code** in your responses. ADRs and decision matrices only. Code goes to Yousef / Hanzla / Omar / Haitham.
|
|
89
|
+
- **Grounding rule (mandatory):** any pricing, fee, rate, market-size, or regulation claim MUST be verified with WebSearch/WebFetch in-session, or explicitly tagged `[unverified — training data]`.
|
|
89
90
|
|
|
90
91
|
## Capabilities
|
|
91
92
|
|
|
@@ -240,6 +240,16 @@ Do NOT skip this step. A council that answers market questions from training dat
|
|
|
240
240
|
Constraints: <regulatory, geographic, or operational limits>
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
3. **MANDATORY ARTIFACT GATE — write the Research context block to disk before spawning any panelist.** This file is the enforcement mechanism: its existence on disk is what proves live research ran, not just prose claiming it did.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
mkdir -p "{paths.sessions_dir}"
|
|
247
|
+
RESEARCH_FILE="{paths.sessions_dir}/$(date +%Y%m%d-%H%M%S)-research.md"
|
|
248
|
+
# Write the "Research context" block above (verbatim) to $RESEARCH_FILE
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Do NOT proceed to Step 3 (panel selection) until `$RESEARCH_FILE` exists on disk with the Research context block written into it. Step 4 (spawn) and Step 5 (synthesis) both depend on this file existing.
|
|
252
|
+
|
|
243
253
|
Also run the minimal codebase scan (config.yaml + README only) so subagents know the team's current capabilities:
|
|
244
254
|
|
|
245
255
|
```bash
|
|
@@ -282,6 +292,8 @@ Use the AskUserQuestion tool (not raw stdin) for the confirmation.
|
|
|
282
292
|
|
|
283
293
|
For each agent id in `panel`, build this prompt. **Before embedding, sanitize the question:** strip any literal `Task(`, `Agent(`, `subagent_type=`, or `system:` tokens that could be misinterpreted as tool calls by the sub-agent (replace with `[filtered]`). This is a low-severity guard — the user already has full access, but it prevents accidental or malicious prompt confusion.
|
|
284
294
|
|
|
295
|
+
**For research-typed questions (`market`/`discovery`/`greenfield`):** `{the summary block}` below MUST be the verbatim content of `$RESEARCH_FILE` written in Step 2's artifact gate — not a paraphrase, not a re-summary. Every panelist reads the same grounded facts.
|
|
296
|
+
|
|
285
297
|
```
|
|
286
298
|
You are being spawned as part of a rcode council session.
|
|
287
299
|
|
|
@@ -289,7 +301,7 @@ You are being spawned as part of a rcode council session.
|
|
|
289
301
|
{sanitized_question}
|
|
290
302
|
|
|
291
303
|
## Observed context
|
|
292
|
-
{the summary block from Step 1 — codebase scan OR
|
|
304
|
+
{the summary block from Step 1 — codebase scan OR the verbatim $RESEARCH_FILE content for research-typed questions}
|
|
293
305
|
|
|
294
306
|
## Session metadata
|
|
295
307
|
- Project: {config.project_name}
|
|
@@ -384,6 +396,14 @@ to resolve or strategic ambiguity to explore.
|
|
|
384
396
|
|
|
385
397
|
## Step 5 — Present responses
|
|
386
398
|
|
|
399
|
+
**Grounding gate (research-typed questions only):** For `market`/`discovery`/`greenfield` questions, before printing anything else, verify `$RESEARCH_FILE` from Step 2 exists on disk (`test -f "$RESEARCH_FILE"`). If it is missing — the research pre-step was skipped or failed silently — open the verdict output (both compact and verbose modes) with this banner as the very first line, before the `COUNCIL VERDICT` header:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
⚠ UNGROUNDED — answered from model knowledge, no live research ran
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Do NOT silently proceed as if research happened. This banner is mandatory whenever the file is missing for a research-typed question; it is never shown for `codebase`/`frontend`/`backend`/etc. questions where no research file is expected.
|
|
406
|
+
|
|
387
407
|
Before saving any artifact, print the panel output inline. Two modes:
|
|
388
408
|
|
|
389
409
|
### Default mode (compact summary)
|
|
@@ -417,6 +437,9 @@ Format:
|
|
|
417
437
|
**Orchestrator note**
|
|
418
438
|
{max 2 sentences — sharpest remaining disagreement OR clearest convergent action}
|
|
419
439
|
|
|
440
|
+
**Data freshness**
|
|
441
|
+
{N} claims live-verified (sources: {comma-separated source names/URLs}) / {M} claims from model knowledge
|
|
442
|
+
|
|
420
443
|
📄 Full transcripts: {artifact path}
|
|
421
444
|
```
|
|
422
445
|
|
|
@@ -425,6 +448,7 @@ Rules for compact mode:
|
|
|
425
448
|
- Convergence table: 2-5 rows, only axes where panelists take a stance. Cells ≤ 6 words.
|
|
426
449
|
- Round 2 deltas: ≤ 15 words each. "Held position" is a valid delta.
|
|
427
450
|
- No section headers beyond the four above. No numbered story breakdowns. No tables from panelists verbatim.
|
|
451
|
+
- **Data freshness footer is mandatory on every synthesis**, not just research-typed questions. Count claims across all panelist responses: a claim is "live-verified" if it cites a source found via WebSearch/WebFetch in this session (Step 2's research file, or a panelist's own in-session lookup); everything else — including anything tagged `[unverified — training data]` by a panelist — counts toward "from model knowledge". For non-research question types with no external claims, use `0 claims live-verified / 0 from model knowledge — no external claims made`.
|
|
428
452
|
|
|
429
453
|
### Verbose mode (`--verbose` flag or `output.verbose: true` in config)
|
|
430
454
|
|
|
@@ -449,6 +473,8 @@ Print Round 1 (and Round 2 if ran) verbatim in panel order. Do NOT summarize.
|
|
|
449
473
|
|
|
450
474
|
---
|
|
451
475
|
**Orchestrator Note:** {max 3 sentences}
|
|
476
|
+
|
|
477
|
+
**Data freshness:** {N} claims live-verified (sources) / {M} claims from model knowledge
|
|
452
478
|
```
|
|
453
479
|
|
|
454
480
|
Before presenting, load the commit format reference:
|
|
@@ -575,6 +601,8 @@ node .rcode/bin/rcode-tools.cjs state record-session
|
|
|
575
601
|
- [ ] Round 2 cross-talk executed (unless consensus or agent deferred)
|
|
576
602
|
- [ ] Session artifact written to `.planning/council-sessions/council-{date}-{slug}.md`
|
|
577
603
|
- [ ] State updated with session record and timestamp
|
|
604
|
+
- [ ] For research-typed questions (`market`/`discovery`/`greenfield`): `$RESEARCH_FILE` written before spawn, or ⚠ UNGROUNDED banner shown
|
|
605
|
+
- [ ] Data freshness footer included in synthesis output
|
|
578
606
|
|
|
579
607
|
## On Error
|
|
580
608
|
|
package/server/dashboard.js
CHANGED
|
@@ -36,6 +36,9 @@ const { renderHtml } = require('./lib/html/shell');
|
|
|
36
36
|
|
|
37
37
|
// ---------- Configuration ----------
|
|
38
38
|
const PORT = parseInt(process.env.PORT || '7717', 10);
|
|
39
|
+
// #969 — the orchestrator's actual port, injected into the client so it never
|
|
40
|
+
// has to hardcode 7718. Defaults match orchestrator.js's own default.
|
|
41
|
+
const ORCH_PORT = parseInt(process.env.ORCH_PORT || '7718', 10);
|
|
39
42
|
const RCODE_DIR = process.env.RCODE_DIR || path.join(process.cwd(), '.rcode');
|
|
40
43
|
const PROJECT_ROOT = path.dirname(RCODE_DIR);
|
|
41
44
|
// Fallback root for agent prompts when rcode is installed as a package (not run
|
|
@@ -147,7 +150,7 @@ function handleRequest(req, res) {
|
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
150
|
-
res.end(JSON.stringify({ token: ORCH_TOKEN }));
|
|
153
|
+
res.end(JSON.stringify({ token: ORCH_TOKEN, orchPort: ORCH_PORT }));
|
|
151
154
|
return;
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -178,7 +181,7 @@ function handleRequest(req, res) {
|
|
|
178
181
|
|
|
179
182
|
if (url === '/' || url === '/index.html') {
|
|
180
183
|
const state = scanState(RCODE_DIR);
|
|
181
|
-
const html = renderHtml(state, ORCH_TOKEN);
|
|
184
|
+
const html = renderHtml(state, ORCH_TOKEN, ORCH_PORT);
|
|
182
185
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
183
186
|
res.end(html);
|
|
184
187
|
return;
|
|
@@ -257,7 +260,7 @@ function spawnOrchestrator() {
|
|
|
257
260
|
console.error('[orch] spawn error:', err.message);
|
|
258
261
|
_orchProc = null;
|
|
259
262
|
});
|
|
260
|
-
console.log(
|
|
263
|
+
console.log(`[orch] orchestrator started (port ${ORCH_PORT})`);
|
|
261
264
|
} catch (err) {
|
|
262
265
|
console.error('[orch] failed to start:', err.message);
|
|
263
266
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { html, useState, useEffect, useRef, useCallback } from '../preact.js';
|
|
17
17
|
import { useStore, setState } from '../store.js';
|
|
18
|
-
import { orchToken, stopSession, cleanSessions,
|
|
18
|
+
import { orchToken, stopSession, cleanSessions, orchWs } from '../orchestrator.js';
|
|
19
19
|
import { showToast } from './shared.js';
|
|
20
20
|
import { Icon } from '../icons-client.js';
|
|
21
21
|
|
|
@@ -106,7 +106,7 @@ export function OrchPanel() {
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
const ws = new WebSocket(
|
|
109
|
-
|
|
109
|
+
orchWs() + '/ws/' + encodeURIComponent(storyId) +
|
|
110
110
|
'?token=' + encodeURIComponent(tok)
|
|
111
111
|
);
|
|
112
112
|
_streams[storyId] = ws;
|
|
@@ -9,18 +9,34 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Store field: state.terminal = { open, storyId, title, minimized, fullscreen }
|
|
11
11
|
* Setting state.terminal via orchestrator.js triggers this component.
|
|
12
|
+
*
|
|
13
|
+
* Two mount points, one singleton terminal:
|
|
14
|
+
* - App.js mounts one instance as a floating overlay (backdrop + sliding
|
|
15
|
+
* panel + minimized pill) on every view.
|
|
16
|
+
* - OrchestrationView.js mounts a second instance with `docked=true` to
|
|
17
|
+
* embed the SAME xterm.js Terminal inline in its right column.
|
|
18
|
+
* Only one instance may touch the DOM at a time — App.js passes
|
|
19
|
+
* `suspend=${view === 'orchestration'}` so its overlay instance goes fully
|
|
20
|
+
* inert (renders null, effects no-op) while Orchestration's docked instance
|
|
21
|
+
* is mounted. `ensureTerm()` reparents the shared xterm DOM node into
|
|
22
|
+
* whichever container asks for it, so the buffer/connection survive the
|
|
23
|
+
* hand-off in both directions.
|
|
12
24
|
*/
|
|
13
25
|
|
|
14
26
|
import { html, useEffect, useRef, useCallback } from '../preact.js';
|
|
15
27
|
import { useStore, setState } from '../store.js';
|
|
16
|
-
import { orchToken, stopSession,
|
|
28
|
+
import { orchToken, stopSession, orchWs } from '../orchestrator.js';
|
|
17
29
|
|
|
18
30
|
// ── Internal state (module-scoped, one panel at a time) ──────────────────────
|
|
19
|
-
// These
|
|
20
|
-
// across panel open/close cycles
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
// These are NOT component state because the xterm instance (and the story it
|
|
32
|
+
// is currently connected to) must persist across panel open/close cycles,
|
|
33
|
+
// Preact re-renders, and — now — across the two XtermPanel mount points
|
|
34
|
+
// (floating overlay vs. docked). Component-local refs would not be shared
|
|
35
|
+
// between those two instances.
|
|
36
|
+
let _term = null;
|
|
37
|
+
let _termFit = null;
|
|
38
|
+
let _termWs = null;
|
|
39
|
+
let _currentStory = null;
|
|
24
40
|
|
|
25
41
|
function setStatus(dotStatus) {
|
|
26
42
|
// Propagate connection status via a store signal so the pill/header can react
|
|
@@ -34,9 +50,23 @@ function _resize() {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Build the xterm instance exactly once; attach to `containerEl`.
|
|
55
|
+
* If the instance already exists but lives under a DIFFERENT container
|
|
56
|
+
* (e.g. the overlay panel had it, and the docked panel is now asking), move
|
|
57
|
+
* its root DOM node into `containerEl` instead of no-oping. xterm.js's root
|
|
58
|
+
* element is a plain DOM node — reparenting it is safe and preserves the
|
|
59
|
+
* scrollback buffer and any live WebSocket connection.
|
|
60
|
+
*/
|
|
38
61
|
function ensureTerm(containerEl) {
|
|
39
|
-
if (_term
|
|
62
|
+
if (_term) {
|
|
63
|
+
if (_term.element && _term.element.parentElement !== containerEl) {
|
|
64
|
+
containerEl.appendChild(_term.element);
|
|
65
|
+
if (_termFit) { try { _termFit.fit(); } catch (_e) {} }
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof Terminal === 'undefined') return;
|
|
40
70
|
_term = new Terminal({
|
|
41
71
|
theme: {
|
|
42
72
|
background: '#0c0c0e', foreground: '#c9d1d9',
|
|
@@ -74,7 +104,7 @@ function connectWs(storyId) {
|
|
|
74
104
|
return;
|
|
75
105
|
}
|
|
76
106
|
setStatus('connecting');
|
|
77
|
-
const url =
|
|
107
|
+
const url = orchWs() + '/ws/' + encodeURIComponent(storyId) + '?token=' + encodeURIComponent(tok);
|
|
78
108
|
const ws = new WebSocket(url);
|
|
79
109
|
_termWs = ws;
|
|
80
110
|
|
|
@@ -98,10 +128,9 @@ function connectWs(storyId) {
|
|
|
98
128
|
|
|
99
129
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
100
130
|
|
|
101
|
-
export function XtermPanel() {
|
|
131
|
+
export function XtermPanel({ docked = false, suspend = false } = {}) {
|
|
102
132
|
const { terminal, termStatus } = useStore();
|
|
103
133
|
const containerRef = useRef(null);
|
|
104
|
-
const currentStoryRef = useRef(null);
|
|
105
134
|
|
|
106
135
|
const t = terminal || {};
|
|
107
136
|
const open = !!t.open;
|
|
@@ -110,28 +139,37 @@ export function XtermPanel() {
|
|
|
110
139
|
const storyId = t.storyId || '';
|
|
111
140
|
const title = t.title || 'Terminal';
|
|
112
141
|
|
|
113
|
-
// Build xterm instance on
|
|
114
|
-
//
|
|
115
|
-
//
|
|
142
|
+
// Build/attach the xterm instance on open; (re)connect only when the
|
|
143
|
+
// focused storyId actually changes. `_currentStory` is module-scoped (not
|
|
144
|
+
// a per-instance ref) so that handing the terminal off between the
|
|
145
|
+
// floating overlay and the docked panel — same storyId, different
|
|
146
|
+
// container — reparents via ensureTerm() without tearing down the
|
|
147
|
+
// connection or clearing the buffer. `suspend` is in the dep array so the
|
|
148
|
+
// OTHER (un-suspending) instance re-runs this effect and reclaims the
|
|
149
|
+
// terminal DOM node when the user navigates away from Orchestration.
|
|
116
150
|
useEffect(() => {
|
|
117
|
-
if (!open || !containerRef.current) return;
|
|
151
|
+
if (suspend || !open || !containerRef.current) return;
|
|
118
152
|
ensureTerm(containerRef.current);
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
153
|
+
const isNewSession = storyId && storyId !== _currentStory;
|
|
154
|
+
if (isNewSession) {
|
|
155
|
+
_currentStory = storyId;
|
|
156
|
+
if (_term) _term.clear();
|
|
122
157
|
connectWs(storyId);
|
|
123
158
|
}
|
|
159
|
+
_resize();
|
|
124
160
|
window.addEventListener('resize', _resize);
|
|
125
161
|
return () => window.removeEventListener('resize', _resize);
|
|
126
|
-
}, [open, storyId]);
|
|
162
|
+
}, [open, storyId, suspend]);
|
|
127
163
|
|
|
128
164
|
// Resize when entering/leaving fullscreen or on open
|
|
129
165
|
useEffect(() => {
|
|
130
|
-
if (open) { setTimeout(_resize, 50); }
|
|
131
|
-
}, [open, fullscreen]);
|
|
166
|
+
if (!suspend && open) { setTimeout(_resize, 50); }
|
|
167
|
+
}, [open, fullscreen, suspend]);
|
|
132
168
|
|
|
133
|
-
// Escape key closes
|
|
169
|
+
// Escape key closes (docked panel has no "close" concept — it just shows
|
|
170
|
+
// the empty state when store.terminal is cleared elsewhere)
|
|
134
171
|
useEffect(() => {
|
|
172
|
+
if (suspend || docked) return;
|
|
135
173
|
function onKey(e) {
|
|
136
174
|
if (e.key === 'Escape' && open && !minimized) {
|
|
137
175
|
setState({ terminal: { ...t, open: false } });
|
|
@@ -139,9 +177,11 @@ export function XtermPanel() {
|
|
|
139
177
|
}
|
|
140
178
|
window.addEventListener('keydown', onKey);
|
|
141
179
|
return () => window.removeEventListener('keydown', onKey);
|
|
142
|
-
}, [open, minimized, t]);
|
|
180
|
+
}, [open, minimized, t, suspend, docked]);
|
|
143
181
|
|
|
144
182
|
const dotCls = 'term-status-dot ' + (termStatus || '');
|
|
183
|
+
// Statuses that mean "output is actively streaming" for the docked live pulse.
|
|
184
|
+
const isLive = open && ['running', 'connecting', 'blocked', 'waiting'].includes(termStatus);
|
|
145
185
|
|
|
146
186
|
// ── Actions ──
|
|
147
187
|
const handleMinimize = useCallback(() => {
|
|
@@ -167,6 +207,41 @@ export function XtermPanel() {
|
|
|
167
207
|
setTimeout(_resize, 50);
|
|
168
208
|
}, [t, fullscreen]);
|
|
169
209
|
|
|
210
|
+
// Fully inert while the sibling instance owns the terminal DOM — no
|
|
211
|
+
// backdrop, no panel, no pill, nothing rendered at all.
|
|
212
|
+
if (suspend) return null;
|
|
213
|
+
|
|
214
|
+
// ── Docked render (Orchestration view's right column) ──
|
|
215
|
+
if (docked) {
|
|
216
|
+
return html`
|
|
217
|
+
<div class="orch-term-dock">
|
|
218
|
+
<div class="orch-term-dock-header">
|
|
219
|
+
<span class="orch-term-dot red"></span>
|
|
220
|
+
<span class="orch-term-dot amber"></span>
|
|
221
|
+
<span class="orch-term-dot green"></span>
|
|
222
|
+
<span class="orch-term-dock-label">xterm${open ? ' · ' + title : ''}</span>
|
|
223
|
+
${isLive ? html`
|
|
224
|
+
<span class="orch-term-dock-live">
|
|
225
|
+
<span class="orch-term-dock-live-dot"></span>live
|
|
226
|
+
</span>
|
|
227
|
+
` : null}
|
|
228
|
+
${open ? html`
|
|
229
|
+
<button class="orch-term-dock-stop" onClick=${handleStop} title="End the agent session">Stop</button>
|
|
230
|
+
` : null}
|
|
231
|
+
</div>
|
|
232
|
+
<div class="orch-term-dock-body">
|
|
233
|
+
${open
|
|
234
|
+
? html`<div ref=${containerRef} class="orch-term-dock-container"></div>`
|
|
235
|
+
: html`
|
|
236
|
+
<div class="orch-term-dock-empty">
|
|
237
|
+
No active execution. Select a command from the Runner picker to begin.
|
|
238
|
+
</div>
|
|
239
|
+
`}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
|
|
170
245
|
// ── Pill (minimized state) ──
|
|
171
246
|
const pill = html`
|
|
172
247
|
<div
|
|
@@ -5,17 +5,27 @@
|
|
|
5
5
|
* Preact store (activeSessions field). Components import these functions
|
|
6
6
|
* directly; no window.* globals needed after Sprint 31.4.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Functions
|
|
9
|
+
* orchHttp() — base URL for orchestrator REST API
|
|
10
|
+
* orchWs() — base URL for orchestrator WebSocket
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { getState, setState } from './store.js';
|
|
14
14
|
import { showToast } from './components/shared.js';
|
|
15
15
|
import { trackBlocked } from './notify.js';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// #969 — the orchestrator port is injected by the server (see shell.js) as
|
|
18
|
+
// window.__ORCH_PORT__, since a dashboard started with ORCH_PORT set (e.g. a
|
|
19
|
+
// second instance under test) spawns its orchestrator on a non-default port.
|
|
20
|
+
// A hardcoded 7718 here would silently drive the wrong orchestrator process.
|
|
21
|
+
// Resolved per-call (not cached at module load) so it works even if a caller
|
|
22
|
+
// loads this module before the inline bootstrap script has run.
|
|
23
|
+
function orchPort() {
|
|
24
|
+
return (typeof window !== 'undefined' && window.__ORCH_PORT__) || 7718;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function orchHttp() { return 'http://localhost:' + orchPort(); }
|
|
28
|
+
export function orchWs() { return 'ws://localhost:' + orchPort(); }
|
|
19
29
|
|
|
20
30
|
// ── Token helpers ─────────────────────────────────────────────────────────────
|
|
21
31
|
|
|
@@ -31,7 +41,10 @@ export function orchToken() {
|
|
|
31
41
|
export function refreshOrchToken() {
|
|
32
42
|
return fetch('/api/orch-token')
|
|
33
43
|
.then(r => r.json())
|
|
34
|
-
.then(d => {
|
|
44
|
+
.then(d => {
|
|
45
|
+
if (d && d.token) window.__ORCH_TOKEN__ = d.token;
|
|
46
|
+
if (d && d.orchPort) window.__ORCH_PORT__ = d.orchPort;
|
|
47
|
+
})
|
|
35
48
|
.catch(() => {});
|
|
36
49
|
}
|
|
37
50
|
|
|
@@ -50,7 +63,7 @@ export function runSession(storyId, cmd, opts) {
|
|
|
50
63
|
body.runner = opts.runner;
|
|
51
64
|
if (opts.model) body.model = opts.model;
|
|
52
65
|
}
|
|
53
|
-
return fetch(
|
|
66
|
+
return fetch(orchHttp() + '/api/run', {
|
|
54
67
|
method: 'POST',
|
|
55
68
|
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
56
69
|
body: JSON.stringify(body),
|
|
@@ -67,7 +80,7 @@ let _runnersPromise = null;
|
|
|
67
80
|
export function fetchRunners() {
|
|
68
81
|
if (_runnersPromise) return _runnersPromise;
|
|
69
82
|
const tok = orchToken();
|
|
70
|
-
_runnersPromise = fetch(
|
|
83
|
+
_runnersPromise = fetch(orchHttp() + '/api/runners', {
|
|
71
84
|
headers: { 'Authorization': 'Bearer ' + tok },
|
|
72
85
|
})
|
|
73
86
|
.then(r => r.json())
|
|
@@ -81,7 +94,7 @@ export function fetchRunners() {
|
|
|
81
94
|
*/
|
|
82
95
|
export function stopSession(storyId) {
|
|
83
96
|
const tok = orchToken();
|
|
84
|
-
return fetch(
|
|
97
|
+
return fetch(orchHttp() + '/api/stop', {
|
|
85
98
|
method: 'POST',
|
|
86
99
|
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
87
100
|
body: JSON.stringify({ storyId }),
|
|
@@ -96,7 +109,7 @@ export function stopSession(storyId) {
|
|
|
96
109
|
function fetchSessionsWithStatus() {
|
|
97
110
|
const tok = orchToken();
|
|
98
111
|
if (!tok) return Promise.resolve({ ok: false, sessions: [] });
|
|
99
|
-
return fetch(
|
|
112
|
+
return fetch(orchHttp() + '/api/sessions', {
|
|
100
113
|
headers: { 'Authorization': 'Bearer ' + tok },
|
|
101
114
|
})
|
|
102
115
|
.then(r => {
|
|
@@ -119,7 +132,7 @@ export function fetchSessions() {
|
|
|
119
132
|
export function fetchHistory() {
|
|
120
133
|
const tok = orchToken();
|
|
121
134
|
if (!tok) return Promise.resolve([]);
|
|
122
|
-
return fetch(
|
|
135
|
+
return fetch(orchHttp() + '/api/history', { headers: { 'Authorization': 'Bearer ' + tok } })
|
|
123
136
|
.then(r => {
|
|
124
137
|
if (r.status === 401) { refreshOrchToken(); return []; }
|
|
125
138
|
return r.json().then(d => (d && d.history) || []);
|
|
@@ -160,7 +173,7 @@ export function isOrchOnline() {
|
|
|
160
173
|
*/
|
|
161
174
|
export function submitRejection(storyId, reason, phase) {
|
|
162
175
|
const tok = orchToken();
|
|
163
|
-
return fetch(
|
|
176
|
+
return fetch(orchHttp() + '/api/reject', {
|
|
164
177
|
method: 'POST',
|
|
165
178
|
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
166
179
|
body: JSON.stringify({ storyId, reason, phase: phase || null }),
|
|
@@ -173,7 +186,7 @@ export function submitRejection(storyId, reason, phase) {
|
|
|
173
186
|
export function fetchRejections() {
|
|
174
187
|
const tok = orchToken();
|
|
175
188
|
if (!tok) return Promise.resolve([]);
|
|
176
|
-
return fetch(
|
|
189
|
+
return fetch(orchHttp() + '/api/rejections', { headers: { 'Authorization': 'Bearer ' + tok } })
|
|
177
190
|
.then(r => r.ok ? r.json().then(d => (d && d.rejections) || []) : [])
|
|
178
191
|
.catch(() => []);
|
|
179
192
|
}
|
|
@@ -184,7 +197,7 @@ export function fetchRejections() {
|
|
|
184
197
|
*/
|
|
185
198
|
export function setTaskStatus(storyId, status) {
|
|
186
199
|
const tok = orchToken();
|
|
187
|
-
return fetch(
|
|
200
|
+
return fetch(orchHttp() + '/api/task-status', {
|
|
188
201
|
method: 'POST',
|
|
189
202
|
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
190
203
|
body: JSON.stringify({ storyId, status }),
|
|
@@ -197,7 +210,7 @@ export function setTaskStatus(storyId, status) {
|
|
|
197
210
|
*/
|
|
198
211
|
export function cleanSessions(olderThanDays = 0) {
|
|
199
212
|
const tok = orchToken();
|
|
200
|
-
return fetch(
|
|
213
|
+
return fetch(orchHttp() + '/api/clean-sessions', {
|
|
201
214
|
method: 'POST',
|
|
202
215
|
headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' },
|
|
203
216
|
body: JSON.stringify({ olderThanDays }),
|