ninja-terminals 2.3.1 → 2.3.2
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/CLAUDE.md +81 -0
- package/ORCHESTRATOR-PROMPT.md +91 -19
- package/README.md +25 -2
- package/agent-send.js +395 -0
- package/cli.js +25 -10
- package/lib/nameGenerator.ts +101 -0
- package/lib/pre-dispatch.js +14 -4
- package/lib/runtime-session.js +337 -0
- package/lib/status-detect.js +68 -4
- package/mcp-server.js +267 -25
- package/ninja-claude-visual.js +13 -0
- package/ninja-codex-visual.js +258 -0
- package/ninja-codex.js +474 -0
- package/ninja-ensure.js +333 -0
- package/ninja-gate.js +340 -0
- package/ninja-login.js +171 -0
- package/ninja-logout.js +42 -0
- package/ninja-visual.js +125 -0
- package/ninja-whoami.js +29 -0
- package/package.json +26 -3
- package/prompts/orchestrator.md +3 -292
- package/public/app.js +197 -4
- package/public/log-viewer.html +463 -0
- package/public/style.css +64 -0
- package/server.js +335 -32
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Ninja Log Viewer</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0a;
|
|
12
|
+
--panel-bg: #111;
|
|
13
|
+
--border: #333;
|
|
14
|
+
--text: #e0e0e0;
|
|
15
|
+
--dim: #666;
|
|
16
|
+
--status-done: #4ade80;
|
|
17
|
+
--status-working: #facc15;
|
|
18
|
+
--status-blocked: #f87171;
|
|
19
|
+
--status-error: #ef4444;
|
|
20
|
+
--status-idle: #94a3b8;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
html, body {
|
|
24
|
+
height: 100%;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
|
|
28
|
+
font-size: 12px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.container {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
height: 100vh;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Status Bar */
|
|
38
|
+
.status-bar {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 8px;
|
|
41
|
+
padding: 8px 12px;
|
|
42
|
+
background: #1a1a1a;
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
overflow-x: auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.status-chip {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 6px;
|
|
52
|
+
padding: 4px 10px;
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
background: var(--panel-bg);
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
font-size: 11px;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.status-chip .dot {
|
|
61
|
+
width: 8px;
|
|
62
|
+
height: 8px;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
background: var(--status-idle);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.status-chip.done .dot { background: var(--status-done); }
|
|
68
|
+
.status-chip.working .dot { background: var(--status-working); animation: pulse 1s infinite; }
|
|
69
|
+
.status-chip.blocked .dot { background: var(--status-blocked); }
|
|
70
|
+
.status-chip.error .dot { background: var(--status-error); }
|
|
71
|
+
.status-chip.idle .dot { background: var(--status-idle); }
|
|
72
|
+
|
|
73
|
+
@keyframes pulse {
|
|
74
|
+
0%, 100% { opacity: 1; }
|
|
75
|
+
50% { opacity: 0.5; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.status-chip .label {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
color: var(--text);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.status-chip .message {
|
|
84
|
+
color: var(--dim);
|
|
85
|
+
max-width: 200px;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Grid */
|
|
91
|
+
.grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: 1fr 1fr;
|
|
94
|
+
grid-template-rows: 1fr 1fr;
|
|
95
|
+
gap: 1px;
|
|
96
|
+
background: var(--border);
|
|
97
|
+
flex: 1;
|
|
98
|
+
min-height: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.pane {
|
|
102
|
+
background: var(--panel-bg);
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
min-height: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.pane-header {
|
|
109
|
+
display: flex;
|
|
110
|
+
justify-content: space-between;
|
|
111
|
+
align-items: center;
|
|
112
|
+
padding: 6px 10px;
|
|
113
|
+
background: #1a1a1a;
|
|
114
|
+
border-bottom: 1px solid var(--border);
|
|
115
|
+
flex-shrink: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pane-title {
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
letter-spacing: 0.5px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.pane-status {
|
|
126
|
+
font-size: 10px;
|
|
127
|
+
padding: 2px 6px;
|
|
128
|
+
border-radius: 3px;
|
|
129
|
+
background: var(--bg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.pane-content {
|
|
133
|
+
flex: 1;
|
|
134
|
+
overflow-y: auto;
|
|
135
|
+
padding: 8px;
|
|
136
|
+
font-size: 11px;
|
|
137
|
+
line-height: 1.4;
|
|
138
|
+
white-space: pre-wrap;
|
|
139
|
+
word-break: break-word;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.pane-content::-webkit-scrollbar {
|
|
143
|
+
width: 6px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.pane-content::-webkit-scrollbar-track {
|
|
147
|
+
background: var(--bg);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.pane-content::-webkit-scrollbar-thumb {
|
|
151
|
+
background: var(--border);
|
|
152
|
+
border-radius: 3px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Log line highlighting */
|
|
156
|
+
.log-line { margin: 1px 0; }
|
|
157
|
+
.log-line.status { color: var(--status-done); font-weight: 600; }
|
|
158
|
+
.log-line.blocked { color: var(--status-blocked); font-weight: 600; }
|
|
159
|
+
.log-line.error { color: var(--status-error); font-weight: 600; }
|
|
160
|
+
.log-line.progress { color: var(--status-working); }
|
|
161
|
+
|
|
162
|
+
/* Empty state */
|
|
163
|
+
.empty {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
height: 100%;
|
|
168
|
+
color: var(--dim);
|
|
169
|
+
font-style: italic;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Connection status */
|
|
173
|
+
.connection-status {
|
|
174
|
+
position: fixed;
|
|
175
|
+
bottom: 12px;
|
|
176
|
+
right: 12px;
|
|
177
|
+
padding: 6px 12px;
|
|
178
|
+
background: var(--panel-bg);
|
|
179
|
+
border: 1px solid var(--border);
|
|
180
|
+
border-radius: 4px;
|
|
181
|
+
font-size: 10px;
|
|
182
|
+
color: var(--dim);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.connection-status.connected { border-color: var(--status-done); color: var(--status-done); }
|
|
186
|
+
.connection-status.disconnected { border-color: var(--status-error); color: var(--status-error); }
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body>
|
|
190
|
+
<div class="container">
|
|
191
|
+
<div class="status-bar" id="status-bar">
|
|
192
|
+
<div class="status-chip idle">
|
|
193
|
+
<span class="dot"></span>
|
|
194
|
+
<span class="label">Connecting...</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="grid" id="grid">
|
|
199
|
+
<div class="pane" data-slot="0">
|
|
200
|
+
<div class="pane-header">
|
|
201
|
+
<span class="pane-title">Terminal 1</span>
|
|
202
|
+
<span class="pane-status">—</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="pane-content"><div class="empty">Waiting for terminal...</div></div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="pane" data-slot="1">
|
|
207
|
+
<div class="pane-header">
|
|
208
|
+
<span class="pane-title">Terminal 2</span>
|
|
209
|
+
<span class="pane-status">—</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="pane-content"><div class="empty">Waiting for terminal...</div></div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="pane" data-slot="2">
|
|
214
|
+
<div class="pane-header">
|
|
215
|
+
<span class="pane-title">Terminal 3</span>
|
|
216
|
+
<span class="pane-status">—</span>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="pane-content"><div class="empty">Waiting for terminal...</div></div>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="pane" data-slot="3">
|
|
221
|
+
<div class="pane-header">
|
|
222
|
+
<span class="pane-title">Terminal 4</span>
|
|
223
|
+
<span class="pane-status">—</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="pane-content"><div class="empty">Waiting for terminal...</div></div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div class="connection-status" id="conn-status">Connecting...</div>
|
|
231
|
+
|
|
232
|
+
<script>
|
|
233
|
+
const MAX_LINES = 500;
|
|
234
|
+
const TOKEN_KEY = 'ninja_token';
|
|
235
|
+
const terminals = new Map();
|
|
236
|
+
const panes = document.querySelectorAll('.pane');
|
|
237
|
+
const statusBar = document.getElementById('status-bar');
|
|
238
|
+
const connStatus = document.getElementById('conn-status');
|
|
239
|
+
|
|
240
|
+
// Get auth token from localStorage (shared with main UI)
|
|
241
|
+
function getToken() {
|
|
242
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Auth headers for fetch
|
|
246
|
+
function authHeaders() {
|
|
247
|
+
const token = getToken();
|
|
248
|
+
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ANSI escape stripper
|
|
252
|
+
function stripAnsi(str) {
|
|
253
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Classify log line
|
|
257
|
+
function classifyLine(line) {
|
|
258
|
+
const stripped = line.toUpperCase();
|
|
259
|
+
if (stripped.includes('STATUS: DONE') || stripped.includes('[DONE]')) return 'status';
|
|
260
|
+
if (stripped.includes('STATUS: BLOCKED') || stripped.includes('[BLOCKED]')) return 'blocked';
|
|
261
|
+
if (stripped.includes('STATUS: ERROR') || stripped.includes('[ERROR]')) return 'error';
|
|
262
|
+
if (stripped.includes('PROGRESS:')) return 'progress';
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Append log line to pane
|
|
267
|
+
function appendLog(pane, text) {
|
|
268
|
+
const content = pane.querySelector('.pane-content');
|
|
269
|
+
const empty = content.querySelector('.empty');
|
|
270
|
+
if (empty) empty.remove();
|
|
271
|
+
|
|
272
|
+
const lines = stripAnsi(text).split('\n');
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
if (!line.trim()) continue;
|
|
275
|
+
const div = document.createElement('div');
|
|
276
|
+
div.className = 'log-line ' + classifyLine(line);
|
|
277
|
+
div.textContent = line;
|
|
278
|
+
content.appendChild(div);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Prune old lines
|
|
282
|
+
while (content.children.length > MAX_LINES) {
|
|
283
|
+
content.removeChild(content.firstChild);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Auto-scroll
|
|
287
|
+
content.scrollTop = content.scrollHeight;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Update status bar
|
|
291
|
+
function updateStatusBar() {
|
|
292
|
+
statusBar.innerHTML = '';
|
|
293
|
+
for (const [id, t] of terminals) {
|
|
294
|
+
const chip = document.createElement('div');
|
|
295
|
+
chip.className = 'status-chip ' + (t.status || 'idle');
|
|
296
|
+
chip.innerHTML = `
|
|
297
|
+
<span class="dot"></span>
|
|
298
|
+
<span class="label">${t.label || 'T' + id}</span>
|
|
299
|
+
<span class="message">${t.taskMessage || t.status || 'idle'}</span>
|
|
300
|
+
`;
|
|
301
|
+
statusBar.appendChild(chip);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (terminals.size === 0) {
|
|
305
|
+
statusBar.innerHTML = '<div class="status-chip idle"><span class="dot"></span><span class="label">No terminals</span></div>';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Connect WebSocket to terminal
|
|
310
|
+
function connectTerminal(id, slot) {
|
|
311
|
+
const pane = panes[slot];
|
|
312
|
+
if (!pane) return;
|
|
313
|
+
|
|
314
|
+
const token = getToken();
|
|
315
|
+
const wsUrl = token
|
|
316
|
+
? `ws://${location.host}/ws/${id}?token=${encodeURIComponent(token)}`
|
|
317
|
+
: `ws://${location.host}/ws/${id}`;
|
|
318
|
+
const ws = new WebSocket(wsUrl);
|
|
319
|
+
|
|
320
|
+
ws.onopen = () => {
|
|
321
|
+
console.log(`Connected to terminal ${id}`);
|
|
322
|
+
const t = terminals.get(id) || {};
|
|
323
|
+
t.ws = ws;
|
|
324
|
+
t.slot = slot;
|
|
325
|
+
terminals.set(id, t);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
ws.onmessage = (e) => {
|
|
329
|
+
appendLog(pane, e.data);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
ws.onclose = () => {
|
|
333
|
+
console.log(`Disconnected from terminal ${id}`);
|
|
334
|
+
const t = terminals.get(id);
|
|
335
|
+
if (t) t.ws = null;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
ws.onerror = (err) => {
|
|
339
|
+
console.error(`WebSocket error for terminal ${id}:`, err);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Connect SSE for status updates
|
|
344
|
+
function connectSSE() {
|
|
345
|
+
const es = new EventSource('/api/events');
|
|
346
|
+
|
|
347
|
+
es.onopen = () => {
|
|
348
|
+
connStatus.textContent = 'Connected';
|
|
349
|
+
connStatus.className = 'connection-status connected';
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
es.onerror = () => {
|
|
353
|
+
connStatus.textContent = 'Disconnected';
|
|
354
|
+
connStatus.className = 'connection-status disconnected';
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
es.addEventListener('status_change', (e) => {
|
|
358
|
+
const data = JSON.parse(e.data);
|
|
359
|
+
const t = terminals.get(data.id) || { label: data.terminal };
|
|
360
|
+
t.status = data.to;
|
|
361
|
+
terminals.set(data.id, t);
|
|
362
|
+
updateStatusBar();
|
|
363
|
+
|
|
364
|
+
// Update pane status
|
|
365
|
+
const pane = panes[t.slot];
|
|
366
|
+
if (pane) {
|
|
367
|
+
pane.querySelector('.pane-status').textContent = data.to;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
es.addEventListener('task_status_change', (e) => {
|
|
372
|
+
const data = JSON.parse(e.data);
|
|
373
|
+
const t = terminals.get(data.id) || { label: data.terminal };
|
|
374
|
+
t.status = data.taskStatus?.state || data.processStatus;
|
|
375
|
+
t.taskMessage = data.taskStatus?.message || '';
|
|
376
|
+
terminals.set(data.id, t);
|
|
377
|
+
updateStatusBar();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Fetch terminals and connect
|
|
382
|
+
async function init() {
|
|
383
|
+
const token = getToken();
|
|
384
|
+
if (!token) {
|
|
385
|
+
connStatus.textContent = 'No auth token - login at main UI first';
|
|
386
|
+
connStatus.className = 'connection-status disconnected';
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch('/api/terminals', { headers: authHeaders() });
|
|
392
|
+
if (!res.ok) {
|
|
393
|
+
throw new Error(`API returned ${res.status}`);
|
|
394
|
+
}
|
|
395
|
+
const data = await res.json();
|
|
396
|
+
|
|
397
|
+
data.forEach((t, i) => {
|
|
398
|
+
if (i >= 4) return; // Max 4 panes
|
|
399
|
+
|
|
400
|
+
terminals.set(t.id, {
|
|
401
|
+
label: t.label,
|
|
402
|
+
status: t.status,
|
|
403
|
+
slot: i
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Update pane header
|
|
407
|
+
const pane = panes[i];
|
|
408
|
+
pane.querySelector('.pane-title').textContent = t.label || `Terminal ${t.id}`;
|
|
409
|
+
pane.querySelector('.pane-status').textContent = t.status;
|
|
410
|
+
|
|
411
|
+
// Connect WebSocket
|
|
412
|
+
connectTerminal(t.id, i);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
updateStatusBar();
|
|
416
|
+
connectSSE();
|
|
417
|
+
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error('Failed to fetch terminals:', err);
|
|
420
|
+
connStatus.textContent = 'Failed to connect';
|
|
421
|
+
connStatus.className = 'connection-status disconnected';
|
|
422
|
+
|
|
423
|
+
// Retry in 3s
|
|
424
|
+
setTimeout(init, 3000);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Poll for new terminals
|
|
429
|
+
setInterval(async () => {
|
|
430
|
+
if (!getToken()) return;
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch('/api/terminals', { headers: authHeaders() });
|
|
433
|
+
if (!res.ok) return;
|
|
434
|
+
const data = await res.json();
|
|
435
|
+
|
|
436
|
+
data.forEach((t, i) => {
|
|
437
|
+
if (i >= 4) return;
|
|
438
|
+
|
|
439
|
+
if (!terminals.has(t.id)) {
|
|
440
|
+
terminals.set(t.id, {
|
|
441
|
+
label: t.label,
|
|
442
|
+
status: t.status,
|
|
443
|
+
slot: i
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const pane = panes[i];
|
|
447
|
+
pane.querySelector('.pane-title').textContent = t.label || `Terminal ${t.id}`;
|
|
448
|
+
pane.querySelector('.pane-status').textContent = t.status;
|
|
449
|
+
pane.querySelector('.pane-content').innerHTML = '';
|
|
450
|
+
|
|
451
|
+
connectTerminal(t.id, i);
|
|
452
|
+
updateStatusBar();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// Ignore polling errors
|
|
457
|
+
}
|
|
458
|
+
}, 5000);
|
|
459
|
+
|
|
460
|
+
init();
|
|
461
|
+
</script>
|
|
462
|
+
</body>
|
|
463
|
+
</html>
|
package/public/style.css
CHANGED
|
@@ -330,6 +330,25 @@ main {
|
|
|
330
330
|
animation: flash-green 0.5s ease-out;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
.terminal-pane.drag-over {
|
|
334
|
+
border-color: var(--accent) !important;
|
|
335
|
+
box-shadow: inset 0 0 30px rgba(232, 169, 23, 0.15);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.terminal-pane.drag-over::after {
|
|
339
|
+
content: 'Drop file here';
|
|
340
|
+
position: absolute;
|
|
341
|
+
top: 50%;
|
|
342
|
+
left: 50%;
|
|
343
|
+
transform: translate(-50%, -50%);
|
|
344
|
+
color: var(--accent);
|
|
345
|
+
font-size: 1.2rem;
|
|
346
|
+
font-weight: 600;
|
|
347
|
+
pointer-events: none;
|
|
348
|
+
z-index: 100;
|
|
349
|
+
text-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
|
|
350
|
+
}
|
|
351
|
+
|
|
333
352
|
/* ── Pane Header ────────────────────────────── */
|
|
334
353
|
|
|
335
354
|
.pane-header {
|
|
@@ -424,6 +443,51 @@ main {
|
|
|
424
443
|
.state-text.starting { color: var(--state-idle); }
|
|
425
444
|
.state-text.exited { color: var(--state-error); opacity: 0.7; }
|
|
426
445
|
|
|
446
|
+
/* Task status indicator (semantic, separate from process status) */
|
|
447
|
+
.pane-task-status {
|
|
448
|
+
display: flex;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: 4px;
|
|
451
|
+
flex-shrink: 0;
|
|
452
|
+
margin-left: 8px;
|
|
453
|
+
padding: 2px 6px;
|
|
454
|
+
border-radius: 3px;
|
|
455
|
+
background: rgba(0,0,0,0.3);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.task-icon {
|
|
459
|
+
font-size: 10px;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.task-icon.pending { color: var(--state-idle); }
|
|
464
|
+
.task-icon.running { color: var(--state-working); }
|
|
465
|
+
.task-icon.done { color: var(--state-done); }
|
|
466
|
+
.task-icon.blocked { color: var(--state-blocked); }
|
|
467
|
+
.task-icon.error { color: var(--state-error); }
|
|
468
|
+
.task-icon.unknown { color: var(--text-secondary); }
|
|
469
|
+
|
|
470
|
+
.task-text {
|
|
471
|
+
font-family: 'Space Grotesk', sans-serif;
|
|
472
|
+
font-size: 10px;
|
|
473
|
+
font-weight: 700;
|
|
474
|
+
text-transform: uppercase;
|
|
475
|
+
letter-spacing: 0.5px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.task-text.pending { color: var(--state-idle); }
|
|
479
|
+
.task-text.running { color: var(--state-working); }
|
|
480
|
+
.task-text.done { color: var(--state-done); }
|
|
481
|
+
.task-text.blocked { color: var(--state-blocked); }
|
|
482
|
+
.task-text.error { color: var(--state-error); }
|
|
483
|
+
.task-text.unknown { color: var(--text-secondary); }
|
|
484
|
+
|
|
485
|
+
/* Task status pane borders (for orchestration visibility) */
|
|
486
|
+
.terminal-pane.task-done { border-color: var(--state-done); }
|
|
487
|
+
.terminal-pane.task-blocked { border-color: var(--state-blocked); }
|
|
488
|
+
.terminal-pane.task-error { border-color: var(--state-error); }
|
|
489
|
+
.terminal-pane.task-running { border-left: 3px solid var(--state-working); }
|
|
490
|
+
|
|
427
491
|
.pane-elapsed {
|
|
428
492
|
font-size: 10px;
|
|
429
493
|
color: var(--text-dim);
|