@jhizzard/termdeck 0.10.3 → 0.11.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/packages/cli/src/init-rumen.js +58 -29
- package/packages/client/public/app.js +218 -4
- package/packages/client/public/index.html +24 -0
- package/packages/client/public/style.css +191 -1
- package/packages/server/src/config.js +49 -0
- package/packages/server/src/index.js +74 -17
- package/packages/server/src/projects-routes.js +119 -0
- package/packages/server/src/pty-reaper.js +297 -0
- package/packages/server/src/setup/index.js +1 -0
- package/packages/server/src/setup/migration-templating.js +76 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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"
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
dotenv,
|
|
35
35
|
supabaseUrl: urlHelper,
|
|
36
36
|
migrations,
|
|
37
|
+
migrationTemplating,
|
|
37
38
|
pgRunner,
|
|
38
39
|
preconditions
|
|
39
40
|
} = require(SETUP_DIR);
|
|
@@ -428,26 +429,53 @@ async function testFunction(projectRef, secrets, dryRun) {
|
|
|
428
429
|
return true;
|
|
429
430
|
}
|
|
430
431
|
|
|
432
|
+
// Sprint 42 T3: bundle of cron-schedule migrations whose `<project-ref>`
|
|
433
|
+
// placeholder must be substituted at apply-time. Both are idempotent
|
|
434
|
+
// (cron.unschedule + cron.schedule), so applying in sequence is safe even
|
|
435
|
+
// when one was already installed. Pre-Sprint 42, only 002 was applied —
|
|
436
|
+
// migration 003 (graph-inference-tick) shipped bundled but unsubstituted
|
|
437
|
+
// and unscheduled, which is part of why Sprint 38 close-out left the
|
|
438
|
+
// graph-inference cron disabled.
|
|
439
|
+
const SCHEDULE_MIGRATIONS = [
|
|
440
|
+
{ matcher: /002.*pg_cron/, label: '002_pg_cron_schedule (rumen-tick)' },
|
|
441
|
+
{ matcher: /003.*graph_inference/, label: '003_graph_inference_schedule (graph-inference-tick)' }
|
|
442
|
+
];
|
|
443
|
+
|
|
431
444
|
async function applySchedule(projectRef, secrets, dryRun) {
|
|
432
|
-
step('Applying pg_cron
|
|
445
|
+
step('Applying pg_cron schedules (rumen-tick + graph-inference-tick)...');
|
|
433
446
|
if (dryRun) { ok('(dry-run)'); return true; }
|
|
434
447
|
|
|
435
448
|
const files = migrations.listRumenMigrations();
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
449
|
+
const planned = [];
|
|
450
|
+
for (const { matcher, label } of SCHEDULE_MIGRATIONS) {
|
|
451
|
+
const file = files.find((f) => matcher.test(path.basename(f)));
|
|
452
|
+
if (!file) { fail(`bundled ${label} is missing`); return false; }
|
|
453
|
+
planned.push({ file, label });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Substitute the project ref into each schedule body. Both bundled
|
|
457
|
+
// migrations ship with the `<project-ref>` placeholder per Rumen's
|
|
458
|
+
// deploy docs; the helper also accepts `{{PROJECT_REF}}` for robustness
|
|
459
|
+
// and refuses to ship an unsubstituted placeholder to the database.
|
|
460
|
+
const substituted = [];
|
|
461
|
+
try {
|
|
462
|
+
for (const { file, label } of planned) {
|
|
463
|
+
const raw = migrations.readFile(file);
|
|
464
|
+
substituted.push({
|
|
465
|
+
sql: migrationTemplating.applyTemplating(raw, { projectRef }),
|
|
466
|
+
label
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
fail(err.message);
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The shipped migrations use Supabase Vault (`vault.decrypted_secrets`)
|
|
475
|
+
// to pull the service-role keys (`rumen_service_role_key` for 002 and
|
|
476
|
+
// `graph_inference_service_role_key` for 003). If a key isn't stored in
|
|
477
|
+
// Vault the corresponding cron call will fail at runtime. We leave that
|
|
478
|
+
// as a post-install step and print a reminder below.
|
|
451
479
|
|
|
452
480
|
let client;
|
|
453
481
|
try {
|
|
@@ -457,20 +485,21 @@ async function applySchedule(projectRef, secrets, dryRun) {
|
|
|
457
485
|
return false;
|
|
458
486
|
}
|
|
459
487
|
try {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return false;
|
|
488
|
+
for (const { sql, label } of substituted) {
|
|
489
|
+
try {
|
|
490
|
+
await pgRunner.run(client, sql);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
fail(`${label}: ${err.message}`);
|
|
493
|
+
process.stderr.write(
|
|
494
|
+
'\nThe schedule SQL failed — the most common cause is that pg_cron or pg_net\n' +
|
|
495
|
+
'is not enabled in the Supabase project. Enable them in Dashboard → Database\n' +
|
|
496
|
+
'→ Extensions, then re-run `termdeck init --rumen --skip-schedule=false`.\n'
|
|
497
|
+
);
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
473
500
|
}
|
|
501
|
+
ok();
|
|
502
|
+
return true;
|
|
474
503
|
} finally {
|
|
475
504
|
try { await client.end(); } catch (_err) { /* ignore */ }
|
|
476
505
|
}
|
|
@@ -91,6 +91,65 @@
|
|
|
91
91
|
setupGuideRail();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// ===== Drag/drop reorder of PTY panels (Sprint 42 T4) =====
|
|
95
|
+
// The grip handle in panel-header-left flips draggable=true on mousedown
|
|
96
|
+
// so an accidental drag inside the xterm region never fires. Drop
|
|
97
|
+
// position is determined by cursor x within the target panel — left half
|
|
98
|
+
// inserts before, right half inserts after — so reordering matches the
|
|
99
|
+
// intent in any grid layout (1x2, 2x2, 2x4, etc.). DOM reorder only;
|
|
100
|
+
// session.creation-order remains canonical for Alt+1…9 and panel-index.
|
|
101
|
+
function setupPanelDragDrop(panel) {
|
|
102
|
+
const handle = panel.querySelector('.panel-drag-handle');
|
|
103
|
+
if (!handle) return;
|
|
104
|
+
|
|
105
|
+
handle.addEventListener('mousedown', () => { panel.draggable = true; });
|
|
106
|
+
// Mouse leaves handle without a drag starting → reset
|
|
107
|
+
handle.addEventListener('mouseleave', () => {
|
|
108
|
+
if (!panel.classList.contains('dragging')) panel.draggable = false;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
panel.addEventListener('dragstart', (e) => {
|
|
112
|
+
if (!panel.draggable) { e.preventDefault(); return; }
|
|
113
|
+
try { e.dataTransfer.effectAllowed = 'move'; } catch (_e) {}
|
|
114
|
+
try { e.dataTransfer.setData('text/plain', panel.id); } catch (_e) {}
|
|
115
|
+
panel.classList.add('dragging');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
panel.addEventListener('dragend', () => {
|
|
119
|
+
panel.classList.remove('dragging');
|
|
120
|
+
panel.draggable = false;
|
|
121
|
+
document.querySelectorAll('.term-panel.drag-over').forEach((p) => p.classList.remove('drag-over'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
panel.addEventListener('dragover', (e) => {
|
|
125
|
+
const dragging = document.querySelector('.term-panel.dragging');
|
|
126
|
+
if (!dragging || dragging === panel) return;
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch (_e) {}
|
|
129
|
+
panel.classList.add('drag-over');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
panel.addEventListener('dragleave', (e) => {
|
|
133
|
+
// Only clear when leaving the panel entirely (not entering a child).
|
|
134
|
+
if (!panel.contains(e.relatedTarget)) panel.classList.remove('drag-over');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
panel.addEventListener('drop', (e) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
panel.classList.remove('drag-over');
|
|
140
|
+
const draggedId = (() => {
|
|
141
|
+
try { return e.dataTransfer.getData('text/plain'); } catch (_e) { return ''; }
|
|
142
|
+
})();
|
|
143
|
+
const dragged = draggedId
|
|
144
|
+
? document.getElementById(draggedId)
|
|
145
|
+
: document.querySelector('.term-panel.dragging');
|
|
146
|
+
if (!dragged || dragged === panel) return;
|
|
147
|
+
const rect = panel.getBoundingClientRect();
|
|
148
|
+
const dropAfter = (e.clientX - rect.left) > rect.width / 2;
|
|
149
|
+
panel.parentNode.insertBefore(dragged, dropAfter ? panel.nextSibling : panel);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
94
153
|
// ===== Create Terminal Panel =====
|
|
95
154
|
function createTerminalPanel(sessionData) {
|
|
96
155
|
const id = sessionData.id;
|
|
@@ -128,6 +187,7 @@
|
|
|
128
187
|
panel.innerHTML = `
|
|
129
188
|
<div class="panel-header">
|
|
130
189
|
<div class="panel-header-left">
|
|
190
|
+
<span class="panel-drag-handle" title="Drag to reorder">⋮⋮</span>
|
|
131
191
|
<span class="status-dot" id="dot-${id}" style="background:${getStatusColor(meta.status)}"></span>
|
|
132
192
|
<span class="panel-type">${getTypeLabel(meta.type)}</span>
|
|
133
193
|
${meta.project ? `<span class="panel-project ${projClass}">${meta.project}</span>` : ''}
|
|
@@ -193,6 +253,11 @@
|
|
|
193
253
|
|
|
194
254
|
document.getElementById('termGrid').appendChild(panel);
|
|
195
255
|
|
|
256
|
+
// Sprint 42 T4: drag/drop reorder. Inject identifier is the session
|
|
257
|
+
// UUID, so DOM reorder is purely visual — Alt+1…9 (creation-order),
|
|
258
|
+
// /api/sessions/:id/input, and reply-form targets are unaffected.
|
|
259
|
+
setupPanelDragDrop(panel);
|
|
260
|
+
|
|
196
261
|
// Create xterm.js instance
|
|
197
262
|
const terminal = new Terminal({
|
|
198
263
|
fontFamily: "'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
|
@@ -265,6 +330,15 @@
|
|
|
265
330
|
if (typeof updateRagIndicator === 'function') updateRagIndicator();
|
|
266
331
|
}
|
|
267
332
|
break;
|
|
333
|
+
case 'projects_changed':
|
|
334
|
+
// Sprint 42 T4: server broadcasts on POST/DELETE /api/projects.
|
|
335
|
+
// Sync the in-memory projects map and re-render the dropdown so
|
|
336
|
+
// other open dashboard tabs stay consistent without a refresh.
|
|
337
|
+
if (msg.projects && state.config) {
|
|
338
|
+
state.config.projects = msg.projects;
|
|
339
|
+
if (typeof rebuildProjectDropdown === 'function') rebuildProjectDropdown();
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
268
342
|
}
|
|
269
343
|
} catch (err) { console.error('[client] ws message parse failed:', err); }
|
|
270
344
|
};
|
|
@@ -1228,6 +1302,17 @@
|
|
|
1228
1302
|
state.focusedId = id;
|
|
1229
1303
|
}
|
|
1230
1304
|
|
|
1305
|
+
// Transfer xterm keyboard focus to the focused panel — without this,
|
|
1306
|
+
// the CSS class is the only thing that changed and keystrokes still
|
|
1307
|
+
// go to whichever element had DOM focus before (often the launcher
|
|
1308
|
+
// input, which submits a NEW terminal on Enter, or the previously
|
|
1309
|
+
// focused panel — leading to "easy to put wrong response into a
|
|
1310
|
+
// chat" reports). Mirrors the focus transfer in focusSessionById.
|
|
1311
|
+
const entry = state.sessions.get(id);
|
|
1312
|
+
if (entry && entry.terminal) {
|
|
1313
|
+
try { entry.terminal.focus(); } catch (err) { /* ignore */ }
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1231
1316
|
// Re-fit all visible terminals
|
|
1232
1317
|
requestAnimationFrame(() => fitAll());
|
|
1233
1318
|
}
|
|
@@ -1270,6 +1355,14 @@
|
|
|
1270
1355
|
if (typeof updateRagIndicator === 'function') updateRagIndicator();
|
|
1271
1356
|
}
|
|
1272
1357
|
break;
|
|
1358
|
+
case 'projects_changed':
|
|
1359
|
+
// Sprint 42 T4: parity with main WS handler. Project add/remove
|
|
1360
|
+
// broadcasts arrive on every ws client; idempotent.
|
|
1361
|
+
if (msg.projects && state.config) {
|
|
1362
|
+
state.config.projects = msg.projects;
|
|
1363
|
+
if (typeof rebuildProjectDropdown === 'function') rebuildProjectDropdown();
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1273
1366
|
}
|
|
1274
1367
|
} catch (err) { console.error('[client] reconnect ws message failed:', err); }
|
|
1275
1368
|
};
|
|
@@ -1541,6 +1634,104 @@
|
|
|
1541
1634
|
}
|
|
1542
1635
|
}
|
|
1543
1636
|
|
|
1637
|
+
// ===== Remove Project modal (Sprint 42 T4) =====
|
|
1638
|
+
// Removes a project from ~/.termdeck/config.yaml. Files on disk at the
|
|
1639
|
+
// project's `path` are NEVER touched — the modal copy makes that explicit
|
|
1640
|
+
// so users don't fear data loss. 409 from the server (live PTY sessions
|
|
1641
|
+
// for that project) prompts the user with a force-override.
|
|
1642
|
+
function openRemoveProjectModal() {
|
|
1643
|
+
const modal = document.getElementById('removeProjectModal');
|
|
1644
|
+
const sel = document.getElementById('rpmSelect');
|
|
1645
|
+
sel.innerHTML = '<option value="">— pick a project —</option>';
|
|
1646
|
+
for (const name of Object.keys(state.config.projects || {})) {
|
|
1647
|
+
const opt = document.createElement('option');
|
|
1648
|
+
opt.value = name;
|
|
1649
|
+
opt.textContent = name;
|
|
1650
|
+
sel.appendChild(opt);
|
|
1651
|
+
}
|
|
1652
|
+
sel.value = '';
|
|
1653
|
+
document.getElementById('rpmConfirm').disabled = true;
|
|
1654
|
+
document.getElementById('rpmConfirm').dataset.force = '';
|
|
1655
|
+
document.getElementById('rpmConfirm').textContent = 'remove project';
|
|
1656
|
+
const warn = document.getElementById('rpmWarning');
|
|
1657
|
+
warn.hidden = true;
|
|
1658
|
+
warn.textContent = '';
|
|
1659
|
+
setRpmStatus('', null);
|
|
1660
|
+
modal.classList.add('open');
|
|
1661
|
+
setTimeout(() => sel.focus(), 50);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function closeRemoveProjectModal() {
|
|
1665
|
+
document.getElementById('removeProjectModal').classList.remove('open');
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function setRpmStatus(msg, kind) {
|
|
1669
|
+
const el = document.getElementById('rpmStatus');
|
|
1670
|
+
if (!el) return;
|
|
1671
|
+
el.textContent = msg || '';
|
|
1672
|
+
el.classList.remove('error', 'ok');
|
|
1673
|
+
if (kind) el.classList.add(kind);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function onRpmSelectChange() {
|
|
1677
|
+
const name = document.getElementById('rpmSelect').value;
|
|
1678
|
+
const btn = document.getElementById('rpmConfirm');
|
|
1679
|
+
btn.disabled = !name;
|
|
1680
|
+
btn.dataset.force = '';
|
|
1681
|
+
btn.textContent = name ? `remove "${name}"` : 'remove project';
|
|
1682
|
+
const warn = document.getElementById('rpmWarning');
|
|
1683
|
+
warn.hidden = true;
|
|
1684
|
+
warn.textContent = '';
|
|
1685
|
+
setRpmStatus('', null);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async function submitRemoveProject() {
|
|
1689
|
+
const name = document.getElementById('rpmSelect').value;
|
|
1690
|
+
if (!name) return;
|
|
1691
|
+
const btn = document.getElementById('rpmConfirm');
|
|
1692
|
+
const force = btn.dataset.force === 'true';
|
|
1693
|
+
btn.disabled = true;
|
|
1694
|
+
setRpmStatus(force ? 'Removing (with force)…' : 'Removing…', null);
|
|
1695
|
+
|
|
1696
|
+
try {
|
|
1697
|
+
const url = `${API}/api/projects/${encodeURIComponent(name)}${force ? '?force=true' : ''}`;
|
|
1698
|
+
const res = await fetch(url, { method: 'DELETE' });
|
|
1699
|
+
const text = await res.text();
|
|
1700
|
+
let body = {};
|
|
1701
|
+
try { body = JSON.parse(text); } catch { body = { error: text }; }
|
|
1702
|
+
|
|
1703
|
+
if (res.status === 409) {
|
|
1704
|
+
const live = body.liveSessions || 0;
|
|
1705
|
+
const warn = document.getElementById('rpmWarning');
|
|
1706
|
+
warn.hidden = false;
|
|
1707
|
+
warn.innerHTML =
|
|
1708
|
+
`<strong>"${name}" has ${live} live PTY session${live === 1 ? '' : 's'}.</strong> ` +
|
|
1709
|
+
`Closing those terminals first is recommended. ` +
|
|
1710
|
+
`Or click <em>remove anyway</em> to force removal — terminals stay open but lose their project tag in config.yaml.`;
|
|
1711
|
+
btn.dataset.force = 'true';
|
|
1712
|
+
btn.textContent = 'remove anyway';
|
|
1713
|
+
btn.disabled = false;
|
|
1714
|
+
setRpmStatus('', null);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
if (!res.ok) {
|
|
1719
|
+
setRpmStatus(`Failed: ${body.error || res.statusText}`, 'error');
|
|
1720
|
+
btn.disabled = false;
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Success — sync in-memory config + dropdown.
|
|
1725
|
+
state.config.projects = body.projects || {};
|
|
1726
|
+
rebuildProjectDropdown();
|
|
1727
|
+
setRpmStatus(`Removed "${name}" ✓ (files on disk untouched)`, 'ok');
|
|
1728
|
+
setTimeout(() => { closeRemoveProjectModal(); }, 900);
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
setRpmStatus(`Failed: ${err.message || err}`, 'error');
|
|
1731
|
+
btn.disabled = false;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1544
1735
|
// ===== Orchestration preview modal (Sprint 37 T3) =====
|
|
1545
1736
|
// The preview button next to the project select shows what
|
|
1546
1737
|
// `termdeck init --project <name>` would create for the currently
|
|
@@ -3703,6 +3894,16 @@
|
|
|
3703
3894
|
if (e.key === 'Escape') { e.preventDefault(); closeAddProjectModal(); }
|
|
3704
3895
|
});
|
|
3705
3896
|
|
|
3897
|
+
// Remove-project modal wiring (Sprint 42 T4)
|
|
3898
|
+
document.getElementById('btnRemoveProject').addEventListener('click', openRemoveProjectModal);
|
|
3899
|
+
document.getElementById('rpmCancel').addEventListener('click', closeRemoveProjectModal);
|
|
3900
|
+
document.getElementById('rpmConfirm').addEventListener('click', submitRemoveProject);
|
|
3901
|
+
document.getElementById('rpmSelect').addEventListener('change', onRpmSelectChange);
|
|
3902
|
+
document.querySelector('#removeProjectModal .remove-project-backdrop').addEventListener('click', closeRemoveProjectModal);
|
|
3903
|
+
document.getElementById('removeProjectModal').addEventListener('keydown', (e) => {
|
|
3904
|
+
if (e.key === 'Escape') { e.preventDefault(); closeRemoveProjectModal(); }
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3706
3907
|
// Orchestration preview modal wiring (Sprint 37 T3)
|
|
3707
3908
|
document.getElementById('btnPreviewProject').addEventListener('click', openPreviewModal);
|
|
3708
3909
|
document.getElementById('promptProject').addEventListener('change', syncPreviewButton);
|
|
@@ -3759,11 +3960,24 @@
|
|
|
3759
3960
|
|
|
3760
3961
|
// Keyboard shortcuts
|
|
3761
3962
|
document.addEventListener('keydown', (e) => {
|
|
3762
|
-
// Tour has priority: Esc exits, ArrowRight/Enter advances, ArrowLeft back
|
|
3963
|
+
// Tour has priority: Esc exits, ArrowRight/Enter advances, ArrowLeft back.
|
|
3964
|
+
// BUT: never swallow Enter/Arrow keys when the user is typing into a
|
|
3965
|
+
// terminal panel or any input/textarea — otherwise terminal Enter
|
|
3966
|
+
// (Claude Code / shell submit) gets eaten by the tour and the user
|
|
3967
|
+
// ends up advancing tour steps when they meant to send a message.
|
|
3968
|
+
// Brad's 2026-04-28 panel-UX report: "Hitting enter from full screen
|
|
3969
|
+
// goes to matrix again" matched this pathway when the v0.10.0 tour
|
|
3970
|
+
// re-fired post-upgrade.
|
|
3763
3971
|
if (tourState.active) {
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3972
|
+
const tgt = e.target;
|
|
3973
|
+
const tag = tgt?.tagName || '';
|
|
3974
|
+
const inEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tgt?.isContentEditable;
|
|
3975
|
+
const inTerminal = tgt?.closest && tgt.closest('.term-panel');
|
|
3976
|
+
if (!inEditable && !inTerminal) {
|
|
3977
|
+
if (e.key === 'Escape') { e.preventDefault(); endTour(); return; }
|
|
3978
|
+
if (e.key === 'ArrowRight' || e.key === 'Enter') { e.preventDefault(); nextTourStep(); return; }
|
|
3979
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); prevTourStep(); return; }
|
|
3980
|
+
}
|
|
3767
3981
|
}
|
|
3768
3982
|
// Ctrl+Shift+N → new terminal
|
|
3769
3983
|
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
|
|
@@ -135,6 +135,7 @@
|
|
|
135
135
|
<option value="">no project</option>
|
|
136
136
|
</select>
|
|
137
137
|
<button class="prompt-add-project" id="btnAddProject" title="Add a new project">+</button>
|
|
138
|
+
<button class="prompt-remove-project" id="btnRemoveProject" title="Remove a project (files on disk are untouched)">−</button>
|
|
138
139
|
<button class="prompt-preview-project" id="btnPreviewProject" title="Preview orchestration scaffolding for the selected project" disabled>preview</button>
|
|
139
140
|
<button class="prompt-launch" id="promptLaunch">launch</button>
|
|
140
141
|
</div>
|
|
@@ -169,6 +170,29 @@
|
|
|
169
170
|
</div>
|
|
170
171
|
</div>
|
|
171
172
|
|
|
173
|
+
<!-- Remove-project modal (Sprint 42 T4). Hidden by default. -->
|
|
174
|
+
<div class="remove-project-modal" id="removeProjectModal" role="dialog" aria-modal="true" aria-labelledby="rpmTitle">
|
|
175
|
+
<div class="remove-project-backdrop"></div>
|
|
176
|
+
<div class="remove-project-card">
|
|
177
|
+
<h3 id="rpmTitle">Remove a project</h3>
|
|
178
|
+
<p class="rpm-help">
|
|
179
|
+
This unregisters the project from <code>~/.termdeck/config.yaml</code>.
|
|
180
|
+
<strong>Files on disk are untouched</strong> — your source code, git history,
|
|
181
|
+
and everything at the project's path stay exactly where they are.
|
|
182
|
+
</p>
|
|
183
|
+
<label>
|
|
184
|
+
<span>Project to remove</span>
|
|
185
|
+
<select id="rpmSelect"><option value="">— pick a project —</option></select>
|
|
186
|
+
</label>
|
|
187
|
+
<div class="rpm-warning" id="rpmWarning" hidden></div>
|
|
188
|
+
<div class="rpm-status" id="rpmStatus"></div>
|
|
189
|
+
<div class="rpm-actions">
|
|
190
|
+
<button class="rpm-cancel" id="rpmCancel">cancel</button>
|
|
191
|
+
<button class="rpm-confirm" id="rpmConfirm" disabled>remove project</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
172
196
|
<!-- Orchestration-preview modal (Sprint 37 T3). Hidden by default. -->
|
|
173
197
|
<div class="preview-project-modal" id="previewProjectModal" role="dialog" aria-modal="true" aria-labelledby="ppmTitle">
|
|
174
198
|
<div class="preview-project-backdrop"></div>
|
|
@@ -297,7 +297,13 @@
|
|
|
297
297
|
/* ===== MAIN GRID ===== */
|
|
298
298
|
.grid-container {
|
|
299
299
|
flex: 1;
|
|
300
|
-
padding
|
|
300
|
+
/* Right padding of 38px reserves the 32px guide-rail collapsed strip
|
|
301
|
+
(position:fixed; right:0) plus 6px breathing. Without this the
|
|
302
|
+
rightmost panel's close (×) button sits underneath the guide
|
|
303
|
+
toggle and becomes unclickable. The expanded-guide state keeps
|
|
304
|
+
its own backdrop/shadow over panels — that's pre-existing
|
|
305
|
+
behavior and not affected by this padding. */
|
|
306
|
+
padding: 6px 38px 6px 6px;
|
|
301
307
|
overflow: hidden;
|
|
302
308
|
display: grid;
|
|
303
309
|
gap: 6px;
|
|
@@ -383,6 +389,31 @@
|
|
|
383
389
|
flex: 1;
|
|
384
390
|
}
|
|
385
391
|
|
|
392
|
+
/* Sprint 42 T4 — drag handle for PTY panel reordering. Only the handle
|
|
393
|
+
is grabbable; the rest of the panel header / xterm body are unaffected. */
|
|
394
|
+
.panel-drag-handle {
|
|
395
|
+
cursor: grab;
|
|
396
|
+
color: var(--tg-text-dim);
|
|
397
|
+
font-size: 12px;
|
|
398
|
+
letter-spacing: -2px;
|
|
399
|
+
user-select: none;
|
|
400
|
+
flex-shrink: 0;
|
|
401
|
+
padding: 0 2px;
|
|
402
|
+
opacity: 0.5;
|
|
403
|
+
transition: opacity 0.15s;
|
|
404
|
+
}
|
|
405
|
+
.panel-drag-handle:hover { opacity: 1; color: var(--tg-accent); }
|
|
406
|
+
.panel-drag-handle:active { cursor: grabbing; }
|
|
407
|
+
.term-panel.dragging {
|
|
408
|
+
opacity: 0.5;
|
|
409
|
+
outline: 2px dashed var(--tg-accent);
|
|
410
|
+
outline-offset: -2px;
|
|
411
|
+
}
|
|
412
|
+
.term-panel.drag-over {
|
|
413
|
+
outline: 2px solid var(--tg-accent);
|
|
414
|
+
outline-offset: -2px;
|
|
415
|
+
}
|
|
416
|
+
|
|
386
417
|
.status-dot {
|
|
387
418
|
width: 8px; height: 8px;
|
|
388
419
|
border-radius: 50%;
|
|
@@ -762,6 +793,156 @@
|
|
|
762
793
|
filter: none;
|
|
763
794
|
}
|
|
764
795
|
|
|
796
|
+
/* ===== REMOVE PROJECT BUTTON + MODAL (Sprint 42 T4) ===== */
|
|
797
|
+
.prompt-remove-project {
|
|
798
|
+
background: var(--tg-bg);
|
|
799
|
+
border: 1px solid var(--tg-border);
|
|
800
|
+
color: var(--tg-text-dim);
|
|
801
|
+
font-size: 18px;
|
|
802
|
+
line-height: 1;
|
|
803
|
+
padding: 0;
|
|
804
|
+
width: 26px;
|
|
805
|
+
height: 26px;
|
|
806
|
+
border-radius: var(--tg-radius-sm);
|
|
807
|
+
cursor: pointer;
|
|
808
|
+
font-family: var(--tg-sans);
|
|
809
|
+
transition: all 0.15s;
|
|
810
|
+
display: flex;
|
|
811
|
+
align-items: center;
|
|
812
|
+
justify-content: center;
|
|
813
|
+
margin-left: 4px;
|
|
814
|
+
}
|
|
815
|
+
.prompt-remove-project:hover {
|
|
816
|
+
border-color: var(--tg-red, #f7768e);
|
|
817
|
+
color: var(--tg-red, #f7768e);
|
|
818
|
+
}
|
|
819
|
+
.remove-project-modal {
|
|
820
|
+
display: none;
|
|
821
|
+
position: fixed;
|
|
822
|
+
inset: 0;
|
|
823
|
+
z-index: 3000;
|
|
824
|
+
align-items: center;
|
|
825
|
+
justify-content: center;
|
|
826
|
+
}
|
|
827
|
+
.remove-project-modal.open { display: flex; }
|
|
828
|
+
.remove-project-backdrop {
|
|
829
|
+
position: absolute;
|
|
830
|
+
inset: 0;
|
|
831
|
+
background: rgba(0, 0, 0, 0.72);
|
|
832
|
+
}
|
|
833
|
+
.remove-project-card {
|
|
834
|
+
position: relative;
|
|
835
|
+
background: var(--tg-surface);
|
|
836
|
+
border: 1px solid var(--tg-accent-dim);
|
|
837
|
+
border-radius: 10px;
|
|
838
|
+
padding: 22px 24px 18px;
|
|
839
|
+
width: 460px;
|
|
840
|
+
max-width: calc(100vw - 40px);
|
|
841
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
|
|
842
|
+
font-family: var(--tg-sans);
|
|
843
|
+
color: var(--tg-text);
|
|
844
|
+
}
|
|
845
|
+
.remove-project-card h3 {
|
|
846
|
+
margin: 0 0 4px;
|
|
847
|
+
font-size: 16px;
|
|
848
|
+
color: var(--tg-accent);
|
|
849
|
+
}
|
|
850
|
+
.remove-project-card .rpm-help {
|
|
851
|
+
margin: 0 0 14px;
|
|
852
|
+
font-size: 12px;
|
|
853
|
+
color: var(--tg-text-dim);
|
|
854
|
+
line-height: 1.5;
|
|
855
|
+
}
|
|
856
|
+
.remove-project-card .rpm-help code {
|
|
857
|
+
background: var(--tg-bg);
|
|
858
|
+
padding: 1px 5px;
|
|
859
|
+
border-radius: 3px;
|
|
860
|
+
font-family: var(--tg-mono);
|
|
861
|
+
font-size: 11px;
|
|
862
|
+
}
|
|
863
|
+
.remove-project-card .rpm-help strong {
|
|
864
|
+
color: var(--tg-green);
|
|
865
|
+
}
|
|
866
|
+
.remove-project-card label {
|
|
867
|
+
display: block;
|
|
868
|
+
margin-bottom: 10px;
|
|
869
|
+
}
|
|
870
|
+
.remove-project-card label > span {
|
|
871
|
+
display: block;
|
|
872
|
+
font-size: 11px;
|
|
873
|
+
color: var(--tg-text-dim);
|
|
874
|
+
margin-bottom: 3px;
|
|
875
|
+
text-transform: uppercase;
|
|
876
|
+
letter-spacing: 0.5px;
|
|
877
|
+
}
|
|
878
|
+
.remove-project-card select {
|
|
879
|
+
width: 100%;
|
|
880
|
+
background: var(--tg-bg);
|
|
881
|
+
border: 1px solid var(--tg-border);
|
|
882
|
+
color: var(--tg-text);
|
|
883
|
+
font-size: 13px;
|
|
884
|
+
padding: 7px 10px;
|
|
885
|
+
border-radius: var(--tg-radius-sm);
|
|
886
|
+
font-family: var(--tg-mono);
|
|
887
|
+
box-sizing: border-box;
|
|
888
|
+
}
|
|
889
|
+
.remove-project-card select:focus {
|
|
890
|
+
outline: none;
|
|
891
|
+
border-color: var(--tg-accent-dim);
|
|
892
|
+
}
|
|
893
|
+
.remove-project-card .rpm-warning {
|
|
894
|
+
background: rgba(247, 118, 142, 0.08);
|
|
895
|
+
border: 1px solid rgba(247, 118, 142, 0.4);
|
|
896
|
+
color: var(--tg-text);
|
|
897
|
+
font-size: 12px;
|
|
898
|
+
padding: 8px 10px;
|
|
899
|
+
border-radius: 4px;
|
|
900
|
+
margin: 6px 0 8px;
|
|
901
|
+
line-height: 1.5;
|
|
902
|
+
}
|
|
903
|
+
.remove-project-card .rpm-status {
|
|
904
|
+
font-size: 12px;
|
|
905
|
+
min-height: 16px;
|
|
906
|
+
margin: 4px 0 8px;
|
|
907
|
+
color: var(--tg-text-dim);
|
|
908
|
+
}
|
|
909
|
+
.remove-project-card .rpm-status.error { color: var(--tg-red); }
|
|
910
|
+
.remove-project-card .rpm-status.ok { color: var(--tg-green); }
|
|
911
|
+
.remove-project-card .rpm-actions {
|
|
912
|
+
display: flex;
|
|
913
|
+
justify-content: flex-end;
|
|
914
|
+
gap: 8px;
|
|
915
|
+
margin-top: 6px;
|
|
916
|
+
}
|
|
917
|
+
.remove-project-card button {
|
|
918
|
+
font-size: 12px;
|
|
919
|
+
font-weight: 600;
|
|
920
|
+
padding: 6px 16px;
|
|
921
|
+
border-radius: 4px;
|
|
922
|
+
cursor: pointer;
|
|
923
|
+
font-family: var(--tg-sans);
|
|
924
|
+
border: 1px solid var(--tg-border);
|
|
925
|
+
}
|
|
926
|
+
.remove-project-card .rpm-cancel {
|
|
927
|
+
background: transparent;
|
|
928
|
+
color: var(--tg-text-dim);
|
|
929
|
+
}
|
|
930
|
+
.remove-project-card .rpm-cancel:hover {
|
|
931
|
+
color: var(--tg-text);
|
|
932
|
+
border-color: var(--tg-border-active);
|
|
933
|
+
}
|
|
934
|
+
.remove-project-card .rpm-confirm {
|
|
935
|
+
background: var(--tg-red, #f7768e);
|
|
936
|
+
color: var(--tg-bg);
|
|
937
|
+
border-color: var(--tg-red, #f7768e);
|
|
938
|
+
}
|
|
939
|
+
.remove-project-card .rpm-confirm:hover { filter: brightness(1.1); }
|
|
940
|
+
.remove-project-card .rpm-confirm:disabled {
|
|
941
|
+
opacity: 0.5;
|
|
942
|
+
cursor: not-allowed;
|
|
943
|
+
filter: none;
|
|
944
|
+
}
|
|
945
|
+
|
|
765
946
|
/* ===== Orchestration preview button + modal (Sprint 37 T3) ===== */
|
|
766
947
|
.prompt-preview-project {
|
|
767
948
|
background: transparent;
|
|
@@ -3439,6 +3620,15 @@
|
|
|
3439
3620
|
padding: 24px;
|
|
3440
3621
|
text-align: center;
|
|
3441
3622
|
}
|
|
3623
|
+
/* Sprint 41 T3 fixed the JS race but missed the CSS specificity gotcha:
|
|
3624
|
+
`.graph-loading { display: flex }` above wins over the UA default
|
|
3625
|
+
`[hidden] { display: none }` (class > attribute). Result: setting
|
|
3626
|
+
`el.hidden = true` from JS did nothing visually, and "Loading graph…"
|
|
3627
|
+
kept rendering behind "No memories yet". Explicit override below. */
|
|
3628
|
+
.graph-loading[hidden],
|
|
3629
|
+
.graph-empty[hidden] {
|
|
3630
|
+
display: none;
|
|
3631
|
+
}
|
|
3442
3632
|
.graph-loading-spinner {
|
|
3443
3633
|
width: 28px;
|
|
3444
3634
|
height: 28px;
|