@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
|
@@ -1,101 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OrchestrationView —
|
|
2
|
+
* OrchestrationView — 2-column Diwan layout.
|
|
3
|
+
*
|
|
4
|
+
* Left rail (300px): view header, Agents card (live rows for whatever the
|
|
5
|
+
* orchestrator is currently running, fed by /api/sessions — replaces the
|
|
6
|
+
* old static command-list "Runner picker"), Pipeline card (live sessions,
|
|
7
|
+
* or a history-status summary when nothing is live).
|
|
8
|
+
* Right column: the docked xterm.js terminal (XtermPanel docked=true) with
|
|
9
|
+
* Run History beneath it.
|
|
3
10
|
*
|
|
4
11
|
* Reads activeSessions from the store (kept fresh by startSessionsPoll in
|
|
5
|
-
* orchestrator.js at 4 s intervals). Terminal
|
|
6
|
-
* XtermPanel opens. Stop calls orchestrator.stopSession.
|
|
12
|
+
* orchestrator.js at 4 s intervals). Terminal focus sets store.terminal so
|
|
13
|
+
* the docked XtermPanel opens/reattaches. Stop calls orchestrator.stopSession.
|
|
7
14
|
*
|
|
8
15
|
* No separate poll timer — if a tighter cadence is needed while this view is
|
|
9
16
|
* open, a local useEffect interval can be added. For now the 4 s global poll
|
|
10
17
|
* is sufficient.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
|
-
import { html, useState
|
|
20
|
+
import { html, useState } from '../preact.js';
|
|
14
21
|
import { useStore } from '../store.js';
|
|
15
|
-
import { stopSession, openTermPanel, ALLOWED_COMMANDS,
|
|
22
|
+
import { stopSession, openTermPanel, ALLOWED_COMMANDS, mergeSessionsAndHistory } from '../orchestrator.js';
|
|
16
23
|
import { openRunnerPicker } from '../components/RunnerPicker.js';
|
|
17
24
|
import { RejectDialog } from '../components/RejectDialog.js';
|
|
25
|
+
import { XtermPanel } from '../components/XtermPanel.js';
|
|
18
26
|
import { orchElapsed, humanDate } from '../util.js';
|
|
19
27
|
import { Icon } from '../icons-client.js';
|
|
20
28
|
|
|
21
|
-
// ── Session card ──────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
// Tooltip text per session status — shown on the colored status dot.
|
|
24
|
-
const DOT_TITLES = {
|
|
25
|
-
running: 'Running — output streaming',
|
|
26
|
-
blocked: 'Blocked — waiting for your input',
|
|
27
|
-
done: 'Done — exited cleanly',
|
|
28
|
-
exited: 'Exited',
|
|
29
|
-
stopped: 'Stopped',
|
|
30
|
-
error: 'Error',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
function OrchCard({ session: s }) {
|
|
34
|
-
const blocked = s.status === 'blocked';
|
|
35
|
-
// 'blocked' is a live PTY (server-side classification of running) — keep
|
|
36
|
-
// the Stop button available for it.
|
|
37
|
-
const running = s.status === 'running' || blocked;
|
|
38
|
-
const waiting = blocked || !!s.waiting;
|
|
39
|
-
const [showReject, setShowReject] = useState(false);
|
|
40
|
-
const cardCls = 'orch-card orch-' + s.status + (waiting ? ' orch-waiting' : '');
|
|
41
|
-
const badge = blocked
|
|
42
|
-
? html`<${Icon} name="alert-triangle" size=${12}/> blocked — needs input`
|
|
43
|
-
: waiting ? html`<${Icon} name="hourglass" size=${12}/> waiting for input` : s.status;
|
|
44
|
-
const dotCls = 'term-status-dot ' + (blocked ? 'blocked' : waiting ? 'waiting' : s.status);
|
|
45
|
-
const dotTitle = DOT_TITLES[s.status] || s.status;
|
|
46
|
-
|
|
47
|
-
function handleTerminal(e) {
|
|
48
|
-
e.stopPropagation();
|
|
49
|
-
openTermPanel(s.storyId, s.storyId);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function handleStop(e) {
|
|
53
|
-
e.stopPropagation();
|
|
54
|
-
stopSession(s.storyId);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return html`
|
|
58
|
-
<div class=${cardCls}>
|
|
59
|
-
<div class="orch-card-head">
|
|
60
|
-
<span class=${dotCls} title=${dotTitle}></span>
|
|
61
|
-
<span class="orch-card-id">${s.storyId}</span>
|
|
62
|
-
${s.runner ? html`
|
|
63
|
-
<span class="runner-badge" title=${'Launched with ' + s.runner + (s.model ? ' (' + s.model + ')' : '')}>
|
|
64
|
-
${s.runner}${s.model ? ' · ' + s.model : ''}
|
|
65
|
-
</span>
|
|
66
|
-
` : null}
|
|
67
|
-
<span class="orch-card-badge">${badge}</span>
|
|
68
|
-
</div>
|
|
69
|
-
<div class="orch-card-cmd">${s.cmd || ''}</div>
|
|
70
|
-
<div class="orch-card-meta">
|
|
71
|
-
<${Icon} name="clock" size=${12}/> ${orchElapsed(s.startTime)}
|
|
72
|
-
${' · '}<${Icon} name="edit-3" size=${12}/> ${s.filesChanged || 0} file${s.filesChanged === 1 ? '' : 's'}
|
|
73
|
-
${' · '}<${Icon} name="eye" size=${12}/> ${s.clients || 0}
|
|
74
|
-
${s.pid ? html` · pid ${s.pid}` : null}
|
|
75
|
-
</div>
|
|
76
|
-
${s.rejection ? html`
|
|
77
|
-
<div class="orch-card-rejection">
|
|
78
|
-
Rejected: ${s.rejection.reason}
|
|
79
|
-
</div>
|
|
80
|
-
` : null}
|
|
81
|
-
<div class="orch-card-actions">
|
|
82
|
-
<button class="term-run-btn outline" onClick=${handleTerminal}>
|
|
83
|
-
<${Icon} name="monitor" size=${14}/> Terminal
|
|
84
|
-
</button>
|
|
85
|
-
${running ? html`
|
|
86
|
-
<button class="term-run-btn danger" onClick=${handleStop}>■ Stop</button>
|
|
87
|
-
` : null}
|
|
88
|
-
${waiting ? html`
|
|
89
|
-
<button class="term-run-btn danger" onClick=${e => { e.stopPropagation(); setShowReject(true); }}>
|
|
90
|
-
<${Icon} name="alert-triangle" size=${14}/> Reject
|
|
91
|
-
</button>
|
|
92
|
-
` : null}
|
|
93
|
-
</div>
|
|
94
|
-
${showReject ? html`<${RejectDialog} session=${s} onClose=${() => setShowReject(false)}/>` : null}
|
|
95
|
-
</div>
|
|
96
|
-
`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
29
|
// ── Sorted session list ───────────────────────────────────────────────────────
|
|
100
30
|
|
|
101
31
|
function sortSessions(sessions) {
|
|
@@ -115,76 +45,215 @@ function sortSessions(sessions) {
|
|
|
115
45
|
});
|
|
116
46
|
}
|
|
117
47
|
|
|
118
|
-
// ──
|
|
48
|
+
// ── Agents card (left rail) ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Two-letter mono abbreviation derived from a storyId (e.g. "cmd-rcode-status"
|
|
52
|
+
* → "RS", "sprint-33.1" → "S3", "phase-33" → "PH"). Strips the synthetic
|
|
53
|
+
* "cmd-" prefix used by the command runner so command sessions abbreviate on
|
|
54
|
+
* their actual command name, not the literal word "cmd".
|
|
55
|
+
*/
|
|
56
|
+
function agentAbbr(storyId) {
|
|
57
|
+
const id = String(storyId || '').replace(/^cmd-/, '');
|
|
58
|
+
const parts = id.split(/[-.]/).filter(Boolean);
|
|
59
|
+
if (parts.length > 1) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
60
|
+
return id.slice(0, 2).toUpperCase();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// How long an ended session keeps showing in the Agents card after it exits,
|
|
64
|
+
// so the transition from "running" to "gone" isn't instant. After this the
|
|
65
|
+
// row falls out of the list; Pipeline's history summary + Run History still
|
|
66
|
+
// have the record.
|
|
67
|
+
const RECENTLY_EXITED_MS = 60000;
|
|
119
68
|
|
|
120
69
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
70
|
+
* Live status for an Agents-card row. classifyStatus() on the server already
|
|
71
|
+
* folds a stalled 'running' PTY into 'blocked'; the 'waiting' flag is a
|
|
72
|
+
* separate, shorter-idle-threshold signal that can be true while status is
|
|
73
|
+
* still 'running'. Both read as the same "needs a look" state here, matching
|
|
74
|
+
* PipelineCard's grouping.
|
|
124
75
|
*/
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
76
|
+
function agentStatusMeta(s) {
|
|
77
|
+
if (s.status === 'blocked' || s.waiting) return { label: 'Blocked', cls: 'blocked' };
|
|
78
|
+
if (s.status === 'running') return { label: 'Running', cls: 'running' };
|
|
79
|
+
return { label: s.status.charAt(0).toUpperCase() + s.status.slice(1), cls: 'exited' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Elapsed / idle / files-changed glance line from the fields /api/sessions already reports. */
|
|
83
|
+
function agentGlance(s) {
|
|
84
|
+
const bits = [orchElapsed(s.startTime) + ' elapsed'];
|
|
85
|
+
if (typeof s.idleSeconds === 'number') bits.push('idle ' + s.idleSeconds + 's');
|
|
86
|
+
if (typeof s.filesChanged === 'number') bits.push(s.filesChanged + ' file' + (s.filesChanged === 1 ? '' : 's') + ' changed');
|
|
87
|
+
return bits.join(' · ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* AgentsCard — replaces the old static "Runner picker" command list. Shows
|
|
92
|
+
* the agents the orchestrator is CURRENTLY RUNNING (live sessions from
|
|
93
|
+
* /api/sessions), each row driven entirely by fields the API already
|
|
94
|
+
* returns: storyId, cmd, status/waiting, startTime, idleSeconds,
|
|
95
|
+
* filesChanged. Clicking a row attaches the docked terminal to that session
|
|
96
|
+
* (same wiring as PipelineCard / #969's fix-orch-live-agents predecessor
|
|
97
|
+
* commit). Command launching (previously this card's only job) moves to the
|
|
98
|
+
* compact footer control — RunnerPicker.js and the confirm-dialog flow are
|
|
99
|
+
* unchanged.
|
|
100
|
+
*/
|
|
101
|
+
function AgentsCard() {
|
|
102
|
+
const { activeSessions, orchOnline, terminal } = useStore();
|
|
128
103
|
const [selected, setSelected] = useState(ALLOWED_COMMANDS[0]?.cmd || '');
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// session is streaming. Managed via useEffect so the timer is cancelled if
|
|
139
|
-
// CommandRunner unmounts before it fires.
|
|
140
|
-
useEffect(() => {
|
|
141
|
-
if (!busy) return;
|
|
142
|
-
const t = setTimeout(() => setBusy(false), 2000);
|
|
143
|
-
return () => clearTimeout(t);
|
|
144
|
-
}, [busy]);
|
|
104
|
+
const orchDown = orchOnline === false;
|
|
105
|
+
const attachedStoryId = terminal && terminal.open ? terminal.storyId : '';
|
|
106
|
+
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const agentRows = sortSessions((activeSessions || []).filter(s => {
|
|
109
|
+
if (s.status === 'running' || s.status === 'blocked') return true;
|
|
110
|
+
const last = Date.parse(s.lastOutputAt || s.startTime || '') || 0;
|
|
111
|
+
return (now - last) < RECENTLY_EXITED_MS;
|
|
112
|
+
}));
|
|
145
113
|
|
|
146
114
|
function handleRun(e) {
|
|
147
|
-
if (!selected ||
|
|
148
|
-
setBusy(true);
|
|
115
|
+
if (!selected || orchDown) return;
|
|
149
116
|
openRunnerPicker(e.currentTarget, { kind: 'command', cmd: selected, title: selected });
|
|
150
117
|
}
|
|
151
118
|
|
|
152
119
|
return html`
|
|
153
|
-
<div class="
|
|
154
|
-
<div class="
|
|
155
|
-
|
|
120
|
+
<div class="orch-runner-card">
|
|
121
|
+
<div class="orch-card-label">Agents</div>
|
|
122
|
+
<div class="orch-runner-list">
|
|
123
|
+
${agentRows.length === 0 ? html`
|
|
124
|
+
<div class="orch-runner-empty">
|
|
125
|
+
<div class="empty">No agents running</div>
|
|
126
|
+
<div class="orch-runner-hint">Use "Run a command" below or launch a story from the Kanban board.</div>
|
|
127
|
+
</div>
|
|
128
|
+
` : agentRows.map(s => {
|
|
129
|
+
const meta = agentStatusMeta(s);
|
|
130
|
+
const isAttached = s.storyId === attachedStoryId;
|
|
131
|
+
return html`
|
|
132
|
+
<div key=${s.storyId}
|
|
133
|
+
class=${'orch-runner-row' + (isAttached ? ' attached' : '')}
|
|
134
|
+
onClick=${() => openTermPanel(s.storyId, s.storyId)}>
|
|
135
|
+
<span class="orch-runner-abbr">${agentAbbr(s.storyId)}</span>
|
|
136
|
+
<div class="orch-runner-info">
|
|
137
|
+
<div class="orch-runner-name">${s.storyId}</div>
|
|
138
|
+
<div class="orch-runner-role">${s.cmd}</div>
|
|
139
|
+
<div class="orch-runner-glance">${agentGlance(s)}</div>
|
|
140
|
+
</div>
|
|
141
|
+
<span class=${'orch-agent-status ' + meta.cls}>${meta.label}</span>
|
|
142
|
+
</div>
|
|
143
|
+
`;
|
|
144
|
+
})}
|
|
156
145
|
</div>
|
|
157
|
-
<div class="
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? html`<${Icon} name="hourglass" size=${14}/> Running…`
|
|
170
|
-
: busy
|
|
171
|
-
? html`<${Icon} name="hourglass" size=${14}/> Starting…`
|
|
172
|
-
: html`<${Icon} name="play" size=${14}/> Run`}
|
|
146
|
+
<div class="orch-runner-card-footer">
|
|
147
|
+
<label class="orch-run-cmd-field">
|
|
148
|
+
<span class="orch-runner-hint">Run a command</span>
|
|
149
|
+
<select class="runner-picker-select" value=${selected}
|
|
150
|
+
onChange=${e => setSelected(e.target.value)}>
|
|
151
|
+
${ALLOWED_COMMANDS.map(({ cmd, label }) => html`
|
|
152
|
+
<option key=${cmd} value=${cmd}>${(label || '').split('—')[0].trim()}</option>
|
|
153
|
+
`)}
|
|
154
|
+
</select>
|
|
155
|
+
</label>
|
|
156
|
+
<button class="term-run-btn" disabled=${orchDown} onClick=${handleRun}>
|
|
157
|
+
<${Icon} name="play" size=${14}/> Run
|
|
173
158
|
</button>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? html`Orchestrator is unreachable — commands cannot run until it is back.`
|
|
178
|
-
: isRunning
|
|
179
|
-
? html`Command is running — output is streaming to the terminal panel.`
|
|
180
|
-
: busy
|
|
181
|
-
? html`Starting — the terminal panel will open shortly.`
|
|
182
|
-
: html`Select a command and press Run. Output streams live to the terminal panel.`}
|
|
159
|
+
${orchDown ? html`
|
|
160
|
+
<div class="orch-runner-hint">Orchestrator unreachable — Run is disabled.</div>
|
|
161
|
+
` : null}
|
|
183
162
|
</div>
|
|
184
163
|
</div>
|
|
185
164
|
`;
|
|
186
165
|
}
|
|
187
166
|
|
|
167
|
+
// ── Pipeline card (left rail) ─────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const PIPELINE_GLYPH = {
|
|
170
|
+
running: '◐', blocked: '!', waiting: '…',
|
|
171
|
+
done: '✓', exited: '✕', stopped: '■', error: '✕',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function PipelineRow({ glyphCls, glyph, label, count, onClick, actions, active }) {
|
|
175
|
+
return html`
|
|
176
|
+
<div class=${'orch-pipeline-row' + (active ? ' active' : '')} onClick=${onClick}>
|
|
177
|
+
<span class=${'orch-pipeline-glyph ' + glyphCls}>${glyph}</span>
|
|
178
|
+
<span class="orch-pipeline-label-text">${label}</span>
|
|
179
|
+
<span class="orch-pipeline-count">${count}</span>
|
|
180
|
+
${actions ? html`<span class="orch-pipeline-actions">${actions}</span>` : null}
|
|
181
|
+
</div>
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* PipelineCard — live sessions (clickable to focus in the docked terminal,
|
|
187
|
+
* with inline Stop / Reject affordances) when any exist; otherwise falls
|
|
188
|
+
* back to a STATUS_ORDER history-count summary so the card stays useful.
|
|
189
|
+
*/
|
|
190
|
+
function PipelineCard() {
|
|
191
|
+
const { activeSessions, history, terminal } = useStore();
|
|
192
|
+
const [rejectFor, setRejectFor] = useState(null);
|
|
193
|
+
const attachedStoryId = terminal && terminal.open ? terminal.storyId : '';
|
|
194
|
+
const live = sortSessions((activeSessions || []).filter(
|
|
195
|
+
s => s.status === 'running' || s.status === 'blocked'
|
|
196
|
+
));
|
|
197
|
+
|
|
198
|
+
let body;
|
|
199
|
+
if (live.length > 0) {
|
|
200
|
+
body = live.map(s => {
|
|
201
|
+
const blocked = s.status === 'blocked';
|
|
202
|
+
const waiting = blocked || !!s.waiting;
|
|
203
|
+
const glyphCls = waiting ? 'blocked' : 'running';
|
|
204
|
+
const actions = html`
|
|
205
|
+
${waiting ? html`
|
|
206
|
+
<button class="orch-pipeline-action-btn danger"
|
|
207
|
+
title="Reject"
|
|
208
|
+
onClick=${e => { e.stopPropagation(); setRejectFor(s); }}>
|
|
209
|
+
<${Icon} name="alert-triangle" size=${11}/>
|
|
210
|
+
</button>
|
|
211
|
+
` : null}
|
|
212
|
+
<button class="orch-pipeline-action-btn danger"
|
|
213
|
+
title="Stop"
|
|
214
|
+
onClick=${e => { e.stopPropagation(); stopSession(s.storyId); }}>■</button>
|
|
215
|
+
`;
|
|
216
|
+
return html`
|
|
217
|
+
<${PipelineRow} key=${s.storyId}
|
|
218
|
+
glyphCls=${glyphCls}
|
|
219
|
+
glyph=${PIPELINE_GLYPH[blocked ? 'blocked' : 'running']}
|
|
220
|
+
label=${s.storyId}
|
|
221
|
+
count=${orchElapsed(s.startTime)}
|
|
222
|
+
active=${s.storyId === attachedStoryId}
|
|
223
|
+
onClick=${() => openTermPanel(s.storyId, s.storyId)}
|
|
224
|
+
actions=${actions}
|
|
225
|
+
/>
|
|
226
|
+
`;
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
const merged = mergeSessionsAndHistory(activeSessions, history);
|
|
230
|
+
const ended = merged.filter(r => r.status !== 'running' && r.status !== 'blocked');
|
|
231
|
+
body = STATUS_ORDER.map(status => {
|
|
232
|
+
const count = ended.filter(r => r.status === status).length;
|
|
233
|
+
if (count === 0) return null;
|
|
234
|
+
return html`
|
|
235
|
+
<${PipelineRow} key=${status}
|
|
236
|
+
glyphCls=${status}
|
|
237
|
+
glyph=${PIPELINE_GLYPH[status] || '·'}
|
|
238
|
+
label=${status}
|
|
239
|
+
count=${count}
|
|
240
|
+
/>
|
|
241
|
+
`;
|
|
242
|
+
});
|
|
243
|
+
if (ended.length === 0) {
|
|
244
|
+
body = html`<div class="empty">No runs yet.</div>`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return html`
|
|
249
|
+
<div class="orch-pipeline-card">
|
|
250
|
+
<div class="orch-pipeline-label">Pipeline</div>
|
|
251
|
+
${body}
|
|
252
|
+
${rejectFor ? html`<${RejectDialog} session=${rejectFor} onClose=${() => setRejectFor(null)}/>` : null}
|
|
253
|
+
</div>
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
|
|
188
257
|
// ── Run history panel ─────────────────────────────────────────────────────────
|
|
189
258
|
|
|
190
259
|
function durationLabel(ms) {
|
|
@@ -194,9 +263,20 @@ function durationLabel(ms) {
|
|
|
194
263
|
return Math.floor(ms / 3600000) + 'h ' + Math.floor((ms % 3600000) / 60000) + 'm';
|
|
195
264
|
}
|
|
196
265
|
|
|
197
|
-
|
|
266
|
+
/**
|
|
267
|
+
* HistoryRow — ended runs are read-only UNLESS the server still holds the
|
|
268
|
+
* session in memory (run.source === 'live', from mergeSessionsAndHistory —
|
|
269
|
+
* true until "Clean sessions" removes it). While that's the case the PTY's
|
|
270
|
+
* scrollback is still attachable over WS, so the row is clickable and
|
|
271
|
+
* reattaches the docked terminal to replay it; otherwise there is nothing to
|
|
272
|
+
* attach to and the row stays static (no persisted log replay exists).
|
|
273
|
+
*/
|
|
274
|
+
function HistoryRow({ run, active }) {
|
|
275
|
+
const attachable = run.source === 'live';
|
|
198
276
|
return html`
|
|
199
|
-
<div class
|
|
277
|
+
<div class=${'hist-row' + (attachable ? ' clickable' : '') + (active ? ' active' : '')}
|
|
278
|
+
key=${run.storyId}
|
|
279
|
+
onClick=${attachable ? () => openTermPanel(run.storyId, run.storyId) : undefined}>
|
|
200
280
|
<span class=${'term-status-dot ' + run.status}></span>
|
|
201
281
|
<span class="hist-row-id">${run.storyId}</span>
|
|
202
282
|
<span class="hist-row-cmd">${run.cmd}</span>
|
|
@@ -209,7 +289,8 @@ function HistoryRow({ run }) {
|
|
|
209
289
|
const STATUS_ORDER = ['done', 'exited', 'stopped', 'error'];
|
|
210
290
|
|
|
211
291
|
function HistoryPanel() {
|
|
212
|
-
const { activeSessions, history } = useStore();
|
|
292
|
+
const { activeSessions, history, terminal } = useStore();
|
|
293
|
+
const attachedStoryId = terminal && terminal.open ? terminal.storyId : '';
|
|
213
294
|
const merged = mergeSessionsAndHistory(activeSessions, history);
|
|
214
295
|
// 'blocked' is a live session (waiting for input), not an ended run.
|
|
215
296
|
const ended = merged.filter(r => r.status !== 'running' && r.status !== 'blocked');
|
|
@@ -257,7 +338,7 @@ function HistoryPanel() {
|
|
|
257
338
|
${[...dateMap.entries()].map(([dateKey, runs]) => html`
|
|
258
339
|
<div key=${dateKey}>
|
|
259
340
|
<div class="hist-date">${dateKey}</div>
|
|
260
|
-
${runs.map(run => html`<${HistoryRow} key=${run.storyId} run=${run}/>`)}
|
|
341
|
+
${runs.map(run => html`<${HistoryRow} key=${run.storyId} run=${run} active=${run.storyId === attachedStoryId}/>`)}
|
|
261
342
|
</div>
|
|
262
343
|
`)}
|
|
263
344
|
</div>
|
|
@@ -271,42 +352,39 @@ function HistoryPanel() {
|
|
|
271
352
|
|
|
272
353
|
export function OrchestrationView() {
|
|
273
354
|
const { activeSessions, orchOnline } = useStore();
|
|
274
|
-
const
|
|
355
|
+
const liveCount = (activeSessions || []).filter(
|
|
356
|
+
s => s.status === 'running' || s.status === 'blocked'
|
|
357
|
+
).length;
|
|
275
358
|
const orchDown = orchOnline === false;
|
|
276
359
|
|
|
277
360
|
return html`
|
|
278
|
-
<div class="view active" id="view-orchestration">
|
|
279
|
-
<div class="
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
⚠ Orchestrator unreachable (port 7718) — Run buttons are disabled.
|
|
287
|
-
Restart the dashboard, or set ORCH_PORT if the port is in use.
|
|
361
|
+
<div class="view active orch-layout" id="view-orchestration">
|
|
362
|
+
<div class="orch-left-rail">
|
|
363
|
+
<div>
|
|
364
|
+
<h1 class="orch-h1">Orchestration</h1>
|
|
365
|
+
<div class="orch-header-sub">
|
|
366
|
+
${liveCount} agent${liveCount === 1 ? '' : 's'} live ·
|
|
367
|
+
${orchDown ? ' runner unreachable' : ' runner connected'}
|
|
368
|
+
</div>
|
|
288
369
|
</div>
|
|
289
|
-
` : null}
|
|
290
|
-
|
|
291
|
-
<${CommandRunner}/>
|
|
292
370
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
Use the Command Runner above, or run <code>/rcode-execute</code> to
|
|
298
|
-
start a phase or sprint.
|
|
371
|
+
${orchDown ? html`
|
|
372
|
+
<div class="orch-down-banner" role="alert">
|
|
373
|
+
⚠ Orchestrator unreachable — Run buttons are disabled.
|
|
374
|
+
Restart the dashboard, or set ORCH_PORT if the port is in use.
|
|
299
375
|
</div>
|
|
300
|
-
|
|
301
|
-
` : html`
|
|
302
|
-
<div class="orch-grid">
|
|
303
|
-
${sessions.map(s => html`
|
|
304
|
-
<${OrchCard} key=${s.storyId} session=${s} />
|
|
305
|
-
`)}
|
|
306
|
-
</div>
|
|
307
|
-
`}
|
|
376
|
+
` : null}
|
|
308
377
|
|
|
309
|
-
|
|
378
|
+
<${AgentsCard}/>
|
|
379
|
+
<${PipelineCard}/>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<div class="orch-right-col">
|
|
383
|
+
<${XtermPanel} docked=${true} />
|
|
384
|
+
<div class="orch-hist-dock">
|
|
385
|
+
<${HistoryPanel}/>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
310
388
|
</div>
|
|
311
389
|
`;
|
|
312
390
|
}
|