@jhizzard/termdeck 1.4.0 → 1.5.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/README.md +1 -1
- package/package.json +3 -5
- package/packages/cli/src/index.js +2 -2
- package/packages/client/public/app.js +98 -8
- package/packages/client/public/style.css +9 -0
- package/packages/server/src/index.js +27 -15
- package/packages/server/src/orchestration-preview.js +1 -1
- package/packages/server/src/session.js +21 -3
- package/packages/server/src/sprint-inject.js +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TermDeck
|
|
2
2
|
|
|
3
|
-
[](https://github.com/jhizzard/termdeck/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
5
|
> **The terminal that remembers what you fixed last month.**
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -39,15 +39,13 @@
|
|
|
39
39
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
40
40
|
"better-sqlite3": "^12.9.0",
|
|
41
41
|
"chalk": "^5.3.0",
|
|
42
|
-
"express": "^
|
|
43
|
-
"open": "^10.0.0",
|
|
42
|
+
"express": "^5.2.1",
|
|
44
43
|
"pg": "^8.20.0",
|
|
45
|
-
"uuid": "^13.0.0",
|
|
46
44
|
"ws": "^8.16.0",
|
|
47
45
|
"yaml": "^2.3.4"
|
|
48
46
|
},
|
|
49
47
|
"devDependencies": {
|
|
50
|
-
"@anthropic-ai/sdk": "^0.
|
|
48
|
+
"@anthropic-ai/sdk": "^0.96.0"
|
|
51
49
|
},
|
|
52
50
|
"keywords": [
|
|
53
51
|
"terminal",
|
|
@@ -164,8 +164,8 @@ function reclaimStalePort(port) {
|
|
|
164
164
|
try { process.kill(parseInt(pid, 10), 'SIGKILL'); } catch (_e) {}
|
|
165
165
|
}
|
|
166
166
|
} else {
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
process.stderr.write(`\n \x1b[31m✗ Port ${port} is in use by a non-TermDeck process (PIDs: ${pids.join(' ')})\x1b[0m\n`);
|
|
168
|
+
process.stderr.write(` \x1b[2mTry a different port: termdeck --port ${port + 1}\x1b[0m\n\n`);
|
|
169
169
|
process.exit(1);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
@@ -438,6 +438,7 @@
|
|
|
438
438
|
<a class="theme-reset" id="theme-reset-${id}" href="javascript:void(0)" onclick="resetTheme('${id}')" title="Revert to project / global default from config.yaml" style="font-size:11px;color:#7aa2f7;text-decoration:none;margin-left:4px;opacity:0.7;cursor:pointer">↺ default</a>
|
|
439
439
|
<button class="ctrl-btn" onclick="focusPanel('${id}')">focus</button>
|
|
440
440
|
<button class="ctrl-btn" onclick="halfPanel('${id}')">half</button>
|
|
441
|
+
<button class="ctrl-btn orch-toggle${isOrchestratorRole(meta.role) ? ' is-orch' : ''}" id="orch-toggle-${id}" type="button" onclick="toggleOrchestratorRole('${id}')" title="${isOrchestratorRole(meta.role) ? 'Unmark this panel as the orchestrator' : 'Mark this panel as the orchestrator — gold border, ORCH badge, pinned row'}">${orchToggleLabel(meta.role)}</button>
|
|
441
442
|
<button class="ctrl-btn reply-toggle" id="reply-btn-${id}" onclick="toggleReplyForm('${id}')" title="Send text to another terminal">reply ▸</button>
|
|
442
443
|
<input type="text" class="ctrl-input" id="ai-${id}" placeholder="Ask about this terminal..." onkeydown="if(event.key==='Enter')askAI('${id}', this.value)">
|
|
443
444
|
</div>
|
|
@@ -729,11 +730,17 @@
|
|
|
729
730
|
return panelProject === selectedFilter;
|
|
730
731
|
}
|
|
731
732
|
|
|
732
|
-
//
|
|
733
|
-
//
|
|
733
|
+
// Sprint 66 T1 (Task 1.1) — the chip row renders whenever there is at
|
|
734
|
+
// least one project bucket, so the project-filter feature is *discoverable*
|
|
735
|
+
// rather than hidden until a second project shows up. Brad's 2026-05-13 v2
|
|
736
|
+
// spec asked for an always-visible rail; his single-live-panel setup sat
|
|
737
|
+
// below the old ≥2 threshold and saw nothing. With one project the row is
|
|
738
|
+
// [ All ] + that one project chip — harmless, and it advertises the filter.
|
|
739
|
+
// `hasNullProject` is retained in the signature for call-site / test
|
|
740
|
+
// compatibility; with ≥1 project the row shows regardless of it, and with
|
|
741
|
+
// zero projects an All-only row carries no filter value so it stays hidden.
|
|
734
742
|
function shouldShowChipRow(projects, hasNullProject) {
|
|
735
|
-
|
|
736
|
-
return n >= 2 || (n >= 1 && hasNullProject === true);
|
|
743
|
+
return (projects || []).length >= 1;
|
|
737
744
|
}
|
|
738
745
|
|
|
739
746
|
// Approach A (Brad's 2026-05-13 spec): orchestrator identity is the
|
|
@@ -742,6 +749,21 @@
|
|
|
742
749
|
return role === 'orchestrator';
|
|
743
750
|
}
|
|
744
751
|
|
|
752
|
+
// Sprint 66 T1 (Task 1.3) — the binary "mark / unmark orchestrator" toggle.
|
|
753
|
+
// nextRoleForToggle: the role the toggle moves a panel TO, given its
|
|
754
|
+
// current role — orchestrator ⇄ unroled (null). A worker/reviewer/auditor
|
|
755
|
+
// panel is "not orchestrator", so the toggle promotes it to orchestrator;
|
|
756
|
+
// it does NOT preserve a prior non-orch role (the affordance is a binary
|
|
757
|
+
// ORCH switch, not a role-history stack — it matches "mark / unmark as
|
|
758
|
+
// orchestrator"). orchToggleLabel: the toggle button's text for a role.
|
|
759
|
+
// Both pure — unit-tested in tests/dashboard-panels-client.test.js.
|
|
760
|
+
function nextRoleForToggle(currentRole) {
|
|
761
|
+
return currentRole === 'orchestrator' ? null : 'orchestrator';
|
|
762
|
+
}
|
|
763
|
+
function orchToggleLabel(role) {
|
|
764
|
+
return role === 'orchestrator' ? 'unmark orch' : 'mark orch';
|
|
765
|
+
}
|
|
766
|
+
|
|
745
767
|
// Belt-and-suspenders for missed panel_exited frames: panel ids the
|
|
746
768
|
// dashboard still has a tile for, but which no longer appear in the
|
|
747
769
|
// server's broadcast session list. Works whether or not T2 filters exited
|
|
@@ -912,10 +934,15 @@
|
|
|
912
934
|
}
|
|
913
935
|
}
|
|
914
936
|
|
|
915
|
-
// 1.2 —
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
937
|
+
// 1.2 — reconcile: keep every tile in the container its role dictates.
|
|
938
|
+
// Re-evaluates isOrchestratorRole() for every panel on each call, so it is
|
|
939
|
+
// the primary mover whenever a role CHANGES — not merely a placement
|
|
940
|
+
// safety net. Sprint 66 T1 (Task 1.2) made meta.role mutable post-spawn
|
|
941
|
+
// (PATCH /api/sessions/:id {role}); a role flip arrives via status_broadcast,
|
|
942
|
+
// updatePanelMeta() merges it into entry.session.meta, scheduleChromeRefresh()
|
|
943
|
+
// runs this, and the panel moves into / out of the ORCH row carrying the
|
|
944
|
+
// panel--role-orch class (the gold border + "ORCH " badge are pure CSS on
|
|
945
|
+
// that class). Returns true if any tile moved.
|
|
919
946
|
function reconcileOrchRow() {
|
|
920
947
|
const orchRow = document.getElementById('orch-pin-row');
|
|
921
948
|
const grid = document.getElementById('termGrid');
|
|
@@ -2177,6 +2204,64 @@
|
|
|
2177
2204
|
if (sel && sel.value !== resolved) sel.value = resolved;
|
|
2178
2205
|
}
|
|
2179
2206
|
|
|
2207
|
+
// Sprint 66 T1 (Task 1.3) — mark / unmark a LIVE panel as the orchestrator
|
|
2208
|
+
// in place. Brad's existing orchestrator panel was spawned with no role and
|
|
2209
|
+
// there was no way to set one short of destroy+recreate via the raw API.
|
|
2210
|
+
// This PATCHes meta.role (the Task 1.2 endpoint); on success the panel
|
|
2211
|
+
// moves into the pinned ORCH row and gains the gold border + "ORCH " badge
|
|
2212
|
+
// with no reload — reconcileOrchRow() (via refreshDashboardChrome) moves it.
|
|
2213
|
+
// Multi-orchestrator is allowed: marking panel B does not unmark panel A
|
|
2214
|
+
// (the ORCH row holds more than one; the operator explicitly unmarks). A
|
|
2215
|
+
// global function — invoked from the Overview-tab button's inline onclick.
|
|
2216
|
+
async function toggleOrchestratorRole(id) {
|
|
2217
|
+
const entry = state.sessions.get(id);
|
|
2218
|
+
if (!entry || entry._mounting || !entry.session) return;
|
|
2219
|
+
const current = entry.session.meta ? entry.session.meta.role : null;
|
|
2220
|
+
const next = nextRoleForToggle(current);
|
|
2221
|
+
const btn = document.getElementById(`orch-toggle-${id}`);
|
|
2222
|
+
if (btn) btn.disabled = true;
|
|
2223
|
+
try {
|
|
2224
|
+
const updated = await api('PATCH', `/api/sessions/${id}`, { role: next });
|
|
2225
|
+
// api() returns the parsed body; a non-2xx body is annotated with
|
|
2226
|
+
// `.error` (annotateApiFailure). The toggle only ever sends a
|
|
2227
|
+
// whitelisted value so a 400 should not occur — but a 404 (panel gone)
|
|
2228
|
+
// or a network failure can, and must not be applied as success.
|
|
2229
|
+
if (updated && updated.error) {
|
|
2230
|
+
console.error('[client] orchestrator-role toggle failed:', updated.error);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
// Apply the authoritative server role from the PATCH response, then
|
|
2234
|
+
// re-route + re-skin the panel. The 2s status_broadcast converges to
|
|
2235
|
+
// the same value (eventually-consistent — same model as changeTheme).
|
|
2236
|
+
if (entry.session.meta) {
|
|
2237
|
+
entry.session.meta.role = (updated && updated.meta) ? updated.meta.role : next;
|
|
2238
|
+
}
|
|
2239
|
+
refreshDashboardChrome();
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
console.error('[client] orchestrator-role toggle error:', err);
|
|
2242
|
+
} finally {
|
|
2243
|
+
if (btn) btn.disabled = false;
|
|
2244
|
+
syncOrchToggle(id);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// Sprint 66 T1 (Task 1.3) — keep a panel's orch-toggle button in sync with
|
|
2249
|
+
// its current meta.role (label, active class, tooltip). Called after a
|
|
2250
|
+
// toggle and on every status_broadcast (updatePanelMeta), so the button is
|
|
2251
|
+
// correct even when the role is changed from another dashboard tab.
|
|
2252
|
+
function syncOrchToggle(id) {
|
|
2253
|
+
const entry = state.sessions.get(id);
|
|
2254
|
+
const btn = document.getElementById(`orch-toggle-${id}`);
|
|
2255
|
+
if (!entry || entry._mounting || !entry.session || !btn) return;
|
|
2256
|
+
const role = entry.session.meta ? entry.session.meta.role : null;
|
|
2257
|
+
const isOrch = isOrchestratorRole(role);
|
|
2258
|
+
btn.textContent = orchToggleLabel(role);
|
|
2259
|
+
btn.classList.toggle('is-orch', isOrch);
|
|
2260
|
+
btn.title = isOrch
|
|
2261
|
+
? 'Unmark this panel as the orchestrator'
|
|
2262
|
+
: 'Mark this panel as the orchestrator — gold border, ORCH badge, pinned row';
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2180
2265
|
async function askAI(id, question) {
|
|
2181
2266
|
if (!question.trim()) return;
|
|
2182
2267
|
const entry = state.sessions.get(id);
|
|
@@ -3405,6 +3490,11 @@
|
|
|
3405
3490
|
entry.session.meta = { ...entry.session.meta, ...meta };
|
|
3406
3491
|
}
|
|
3407
3492
|
|
|
3493
|
+
// Sprint 66 T1 (Task 1.3) — re-sync the orch-toggle button from the just-
|
|
3494
|
+
// merged role, so a role changed from another dashboard tab is reflected
|
|
3495
|
+
// here too (the per-tab toggle path syncs in its own finally block).
|
|
3496
|
+
syncOrchToggle(id);
|
|
3497
|
+
|
|
3408
3498
|
const dot = document.getElementById(`dot-${id}`);
|
|
3409
3499
|
const status = document.getElementById(`status-${id}`);
|
|
3410
3500
|
const metaLast = document.getElementById(`meta-last-${id}`);
|
|
@@ -694,6 +694,15 @@
|
|
|
694
694
|
.ctrl-btn:hover { color: var(--tg-text); border-color: var(--tg-border-active); }
|
|
695
695
|
.ctrl-btn.active { color: var(--tg-accent); border-color: var(--tg-accent-dim); }
|
|
696
696
|
|
|
697
|
+
/* Sprint 66 T1 (Task 1.3) — the "mark / unmark orchestrator" toggle in the
|
|
698
|
+
Overview controls. When the panel IS the orchestrator the button takes
|
|
699
|
+
the gold/amber accent — the same hue as the ORCH-pin border + badge — so
|
|
700
|
+
the control and the panel treatment read as one feature. */
|
|
701
|
+
.ctrl-btn.orch-toggle.is-orch {
|
|
702
|
+
color: var(--tg-accent-orch, #d4a017);
|
|
703
|
+
border-color: var(--tg-accent-orch, #d4a017);
|
|
704
|
+
}
|
|
705
|
+
|
|
697
706
|
.theme-select {
|
|
698
707
|
background: var(--tg-bg);
|
|
699
708
|
border: 1px solid var(--tg-border);
|
|
@@ -10,7 +10,6 @@ const os = require('os');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const dns = require('dns');
|
|
12
12
|
const { spawn: spawnChild } = require('child_process');
|
|
13
|
-
const { v4: uuidv4 } = require('uuid');
|
|
14
13
|
const { createCachedLookup, createFailureLogger } = require('./rumen-pool-resilience');
|
|
15
14
|
|
|
16
15
|
// Conditional imports (graceful fallback if not installed yet)
|
|
@@ -29,7 +28,7 @@ try {
|
|
|
29
28
|
console.error('[db] better-sqlite3 native ABI mismatch (Node was upgraded after install).');
|
|
30
29
|
console.error('[db] TermDeck cannot serve memory features without a working SQLite.');
|
|
31
30
|
console.error('[db] Fix:');
|
|
32
|
-
|
|
31
|
+
process.stderr.write(' cd "$(npm root -g)/@jhizzard/termdeck" && npm rebuild better-sqlite3\n');
|
|
33
32
|
console.error('[db] Then restart TermDeck. Aborting.');
|
|
34
33
|
process.exit(1);
|
|
35
34
|
}
|
|
@@ -210,13 +209,13 @@ function _defaultSpawnSessionEndHookImpl(hookPath, payload, env) {
|
|
|
210
209
|
env,
|
|
211
210
|
});
|
|
212
211
|
child.on('error', (err) => {
|
|
213
|
-
console.error('[
|
|
212
|
+
console.error('[panel-close] hook spawn error:', err && err.message ? err.message : err);
|
|
214
213
|
});
|
|
215
214
|
try {
|
|
216
215
|
child.stdin.write(JSON.stringify(payload));
|
|
217
216
|
child.stdin.end();
|
|
218
217
|
} catch (err) {
|
|
219
|
-
console.error('[
|
|
218
|
+
console.error('[panel-close] hook stdin write failed:', err && err.message ? err.message : err);
|
|
220
219
|
}
|
|
221
220
|
child.unref();
|
|
222
221
|
return child;
|
|
@@ -292,7 +291,7 @@ async function onPanelClose(session) {
|
|
|
292
291
|
...readTermdeckSecretsForPty(),
|
|
293
292
|
});
|
|
294
293
|
} catch (err) {
|
|
295
|
-
console.error('[
|
|
294
|
+
console.error('[panel-close] error:', err && err.message ? err.message : err);
|
|
296
295
|
}
|
|
297
296
|
}
|
|
298
297
|
|
|
@@ -367,7 +366,7 @@ async function onPanelPeriodicCapture(session) {
|
|
|
367
366
|
session._periodicCapture.lastSize = stat.size;
|
|
368
367
|
session._periodicCapture.lastFireMs = Date.now();
|
|
369
368
|
} catch (err) {
|
|
370
|
-
console.error('[
|
|
369
|
+
console.error('[periodic-capture] error:', err && err.message ? err.message : err);
|
|
371
370
|
}
|
|
372
371
|
}
|
|
373
372
|
|
|
@@ -424,7 +423,7 @@ function _getT2DestFor() {
|
|
|
424
423
|
|
|
425
424
|
function _termdeckVersion() {
|
|
426
425
|
try { return require('../../../package.json').version; }
|
|
427
|
-
catch { return '0.0.0'; }
|
|
426
|
+
catch (err) { console.error('[version] package.json read failed:', err); return '0.0.0'; }
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
// Sprint 60 v1.0.14 (Item 3) — safe PTY resize. Brad's 2026-05-07 r730 crash
|
|
@@ -432,7 +431,7 @@ function _termdeckVersion() {
|
|
|
432
431
|
// EBADF/ENOTTY` per 13h uptime. Race: WS `resize` message arrives for a PTY
|
|
433
432
|
// that pty-reaper has already closed (or the child has exited), and
|
|
434
433
|
// `pty.resize()` ioctls a stale fd. The error is race-expected, not a bug,
|
|
435
|
-
// but the noisy
|
|
434
|
+
// but the noisy stderr trace pollutes diagnostics and obscures real
|
|
436
435
|
// errors. This helper guards against the race and downgrades the known
|
|
437
436
|
// race-class errors (EBADF, ENOTTY) to a silent return. Set
|
|
438
437
|
// TERMDECK_DEBUG_PTY_RACES=1 to log to console.debug for diagnostics.
|
|
@@ -1457,7 +1456,7 @@ function createServer(config) {
|
|
|
1457
1456
|
session._periodicCapture = { lastSize: 0, lastFireMs: 0, timer: null };
|
|
1458
1457
|
session._periodicCapture.timer = setInterval(() => {
|
|
1459
1458
|
onPanelPeriodicCapture(session).catch((err) => {
|
|
1460
|
-
console.error('[
|
|
1459
|
+
console.error('[periodic-capture] async error:', err && err.message ? err.message : err);
|
|
1461
1460
|
});
|
|
1462
1461
|
}, intervalMs);
|
|
1463
1462
|
// Don't keep the event loop alive solely for this timer — the PTY
|
|
@@ -1549,7 +1548,7 @@ function createServer(config) {
|
|
|
1549
1548
|
// skip-claude + skip-when-no-transcript. Fire-and-forget; any
|
|
1550
1549
|
// error logs and never blocks teardown.
|
|
1551
1550
|
onPanelClose(session).catch((err) => {
|
|
1552
|
-
console.error('[
|
|
1551
|
+
console.error('[panel-close] async error:', err && err.message ? err.message : err);
|
|
1553
1552
|
});
|
|
1554
1553
|
|
|
1555
1554
|
// Sprint 59 T4-CODEX UPLOAD-AUDIT-CONCERN closure: blow away the
|
|
@@ -1754,7 +1753,19 @@ function createServer(config) {
|
|
|
1754
1753
|
|
|
1755
1754
|
// PATCH /api/sessions/:id - update session metadata
|
|
1756
1755
|
app.patch('/api/sessions/:id', (req, res) => {
|
|
1757
|
-
|
|
1756
|
+
// Sprint 66 T1 (Task 1.2) — `role` is PATCH-mutable so an operator can tag
|
|
1757
|
+
// a live panel as orchestrator in place. Validate it exactly as POST
|
|
1758
|
+
// /api/sessions does (index.js — the `invalid_role` 400 above): an absent
|
|
1759
|
+
// field is fine, any present value must be in ALLOWED_SESSION_ROLES
|
|
1760
|
+
// (orchestrator/worker/reviewer/auditor/null) — an unknown value is a 400
|
|
1761
|
+
// so a typo surfaces immediately rather than silently mis-tagging the
|
|
1762
|
+
// panel. Validation runs BEFORE updateMeta so a bad role never reaches the
|
|
1763
|
+
// whitelist apply or the SQLite write.
|
|
1764
|
+
const body = req.body || {};
|
|
1765
|
+
if (body.role !== undefined && !ALLOWED_SESSION_ROLES.includes(body.role)) {
|
|
1766
|
+
return res.status(400).json({ ok: false, code: 'invalid_role', allowed: ALLOWED_SESSION_ROLES });
|
|
1767
|
+
}
|
|
1768
|
+
const session = sessions.updateMeta(req.params.id, body);
|
|
1758
1769
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
1759
1770
|
res.json(session.toJSON());
|
|
1760
1771
|
});
|
|
@@ -2023,7 +2034,7 @@ function createServer(config) {
|
|
|
2023
2034
|
return res.status(410).json({ error: 'PTY is gone (session exited)' });
|
|
2024
2035
|
}
|
|
2025
2036
|
|
|
2026
|
-
const { cols, rows } = req.body;
|
|
2037
|
+
const { cols, rows } = req.body || {};
|
|
2027
2038
|
try {
|
|
2028
2039
|
const resized = safelyResizePty(session, cols, rows);
|
|
2029
2040
|
if (!resized) {
|
|
@@ -2603,7 +2614,7 @@ function createServer(config) {
|
|
|
2603
2614
|
|
|
2604
2615
|
// POST /api/ai/query - query Mnestra memory via the bridge (direct|webhook|mcp)
|
|
2605
2616
|
app.post('/api/ai/query', async (req, res) => {
|
|
2606
|
-
let { question, sessionId, project } = req.body;
|
|
2617
|
+
let { question, sessionId, project } = req.body || {};
|
|
2607
2618
|
if (!question) return res.status(400).json({ error: 'Missing question' });
|
|
2608
2619
|
|
|
2609
2620
|
let searchAll = false;
|
|
@@ -2758,8 +2769,9 @@ function createServer(config) {
|
|
|
2758
2769
|
});
|
|
2759
2770
|
}, 2000);
|
|
2760
2771
|
|
|
2761
|
-
// Fallback route → serve index.html
|
|
2762
|
-
|
|
2772
|
+
// Fallback route → serve index.html. Express 5: named wildcard '/{*splat}'
|
|
2773
|
+
// (path-to-regexp v8 — a bare '*' throws at registration; this matches all paths incl. root).
|
|
2774
|
+
app.get('/{*splat}', (req, res) => {
|
|
2763
2775
|
res.sendFile(path.join(clientDir, 'index.html'));
|
|
2764
2776
|
});
|
|
2765
2777
|
|
|
@@ -187,7 +187,7 @@ async function generateScaffolding({ name, projects, cwd, force, initProject, te
|
|
|
187
187
|
// Refuse on existing non-empty dir without force, mirroring T2's CLI semantics.
|
|
188
188
|
if (fs.existsSync(targetPath)) {
|
|
189
189
|
const entries = (() => {
|
|
190
|
-
try { return fs.readdirSync(targetPath); } catch { return []; }
|
|
190
|
+
try { return fs.readdirSync(targetPath); } catch (err) { console.error('[orch-preview] readdir failed:', err); return []; }
|
|
191
191
|
})();
|
|
192
192
|
const nonEmpty = entries.length > 0;
|
|
193
193
|
if (nonEmpty && !force) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// metadata broadcast in index.js untouched — `s.meta.theme` already returns
|
|
11
11
|
// the right thing whenever index.js dereferences it.
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { randomUUID } = require('crypto');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveTheme } = require('./theme-resolver');
|
|
@@ -135,7 +135,7 @@ const PATTERNS = {
|
|
|
135
135
|
|
|
136
136
|
class Session {
|
|
137
137
|
constructor(options) {
|
|
138
|
-
this.id = options.id ||
|
|
138
|
+
this.id = options.id || randomUUID();
|
|
139
139
|
this.pid = null;
|
|
140
140
|
this.pty = null;
|
|
141
141
|
this.ws = null;
|
|
@@ -634,7 +634,15 @@ class SessionManager {
|
|
|
634
634
|
'label',
|
|
635
635
|
'project',
|
|
636
636
|
'ragEnabled',
|
|
637
|
-
'flashbackEnabled'
|
|
637
|
+
'flashbackEnabled',
|
|
638
|
+
// Sprint 66 T1 (Task 1.2) — `role` is now PATCH-mutable so an operator can
|
|
639
|
+
// tag a live panel as orchestrator in place (Brad's existing orch panel
|
|
640
|
+
// was spawned with no role and had no way to set one short of a raw-API
|
|
641
|
+
// destroy + recreate). The PATCH /api/sessions/:id route validates the
|
|
642
|
+
// value against ALLOWED_SESSION_ROLES before this whitelist is consulted —
|
|
643
|
+
// the same "route validates, model trusts" boundary as POST /api/sessions
|
|
644
|
+
// and the Session constructor.
|
|
645
|
+
'role'
|
|
638
646
|
]);
|
|
639
647
|
|
|
640
648
|
updateMeta(id, updates) {
|
|
@@ -657,6 +665,16 @@ class SessionManager {
|
|
|
657
665
|
.run(applied.theme == null ? null : applied.theme, id);
|
|
658
666
|
}
|
|
659
667
|
|
|
668
|
+
// Sprint 66 T1 (Task 1.2) — persist a role change to SQLite so a panel
|
|
669
|
+
// tagged orchestrator via PATCH keeps the role across a server restart /
|
|
670
|
+
// dashboard reload, exactly as a spawn-time role does. create() writes the
|
|
671
|
+
// `role` column on INSERT; this is its UPDATE counterpart. The column was
|
|
672
|
+
// added by Sprint 65 T2 (CREATE TABLE + a PRAGMA-guarded ALTER migration).
|
|
673
|
+
if ('role' in applied && this.db) {
|
|
674
|
+
this.db.prepare('UPDATE sessions SET role = ? WHERE id = ?')
|
|
675
|
+
.run(applied.role == null ? null : applied.role, id);
|
|
676
|
+
}
|
|
677
|
+
|
|
660
678
|
this._emit('session:updated', session);
|
|
661
679
|
return session;
|
|
662
680
|
}
|
|
@@ -233,7 +233,7 @@ async function injectSprintPrompts({
|
|
|
233
233
|
} else {
|
|
234
234
|
anyPending = true;
|
|
235
235
|
}
|
|
236
|
-
} catch {
|
|
236
|
+
} catch (_err) {
|
|
237
237
|
anyPending = true;
|
|
238
238
|
}
|
|
239
239
|
}
|
|
@@ -257,7 +257,7 @@ async function injectSprintPrompts({
|
|
|
257
257
|
const s = await getStatus(lane.sessionId);
|
|
258
258
|
lane.finalStatus = s && s.status ? s.status : lane.finalStatus;
|
|
259
259
|
if (lane.finalStatus === 'thinking') lane.verified = true;
|
|
260
|
-
} catch {
|
|
260
|
+
} catch (_err) {
|
|
261
261
|
// ignore
|
|
262
262
|
}
|
|
263
263
|
}
|