@jhizzard/termdeck 1.6.1 → 1.8.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/doctor.js +100 -0
- package/packages/cli/src/init-mnestra.js +50 -6
- package/packages/cli/src/init-rumen.js +3 -3
- package/packages/client/public/app.js +341 -30
- package/packages/client/public/index.html +0 -1
- package/packages/client/public/style.css +2 -31
- package/packages/server/src/agent-adapters/agy.js +396 -0
- package/packages/server/src/agent-adapters/gemini.js +309 -42
- package/packages/server/src/agent-adapters/grok-models.js +112 -76
- package/packages/server/src/agent-adapters/index.js +19 -0
- package/packages/server/src/agent-adapters/web-chat-grok.js +259 -0
- package/packages/server/src/index.js +572 -10
- package/packages/server/src/setup/audit-upgrade.js +3 -3
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +1 -1
- package/packages/stack-installer/assets/hooks/memory-session-end.js +73 -32
|
@@ -472,6 +472,14 @@
|
|
|
472
472
|
// /api/sessions/:id/input, and reply-form targets are unaffected.
|
|
473
473
|
setupPanelDragDrop(panel);
|
|
474
474
|
|
|
475
|
+
// Sprint 72 T3 — web-chat (Grok) panels render a live screencast canvas +
|
|
476
|
+
// an inject input box instead of an xterm. Branch AFTER the shared chrome
|
|
477
|
+
// (header/meta/drawer) is built, then return; the entire xterm path below
|
|
478
|
+
// stays byte-identical for every other panel type.
|
|
479
|
+
if (meta.type === 'web-chat') {
|
|
480
|
+
return mountWebChatPanel(id, sessionData, panel);
|
|
481
|
+
}
|
|
482
|
+
|
|
475
483
|
// Create xterm.js instance
|
|
476
484
|
const terminal = new Terminal({
|
|
477
485
|
fontFamily: "'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
|
@@ -653,6 +661,297 @@
|
|
|
653
661
|
return { terminal, ws, fitAddon };
|
|
654
662
|
}
|
|
655
663
|
|
|
664
|
+
// ===== Sprint 72 T3: web-chat (Grok) canvas panel =====
|
|
665
|
+
// A web-chat panel mirrors a real, logged-in headful grok.com tab: T1's CDP
|
|
666
|
+
// screencast frames paint to a <canvas>; an input box injects a prompt into
|
|
667
|
+
// the live composer (T2 routes {type:'input'} → grok.inject); and canvas
|
|
668
|
+
// mouse/wheel events forward back to the tab ({type:'web-chat-input'} →
|
|
669
|
+
// handle.sendInput) so the human can drive it in-deck. The xterm render path
|
|
670
|
+
// (createTerminalPanel, above) is untouched for every non-web-chat panel.
|
|
671
|
+
|
|
672
|
+
function ensureWebChatStyles() {
|
|
673
|
+
if (document.getElementById('wc-styles')) return;
|
|
674
|
+
const style = document.createElement('style');
|
|
675
|
+
style.id = 'wc-styles';
|
|
676
|
+
style.textContent = [
|
|
677
|
+
'.panel-terminal.web-chat-terminal{display:flex;flex-direction:column;height:100%;min-height:0;background:#000;}',
|
|
678
|
+
'.web-chat-stage{flex:1;min-height:0;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#000;}',
|
|
679
|
+
'.web-chat-canvas{display:block;max-width:100%;max-height:100%;outline:none;}',
|
|
680
|
+
'.web-chat-input-bar{display:flex;gap:6px;padding:6px;border-top:1px solid #1f2335;background:#16161e;}',
|
|
681
|
+
'.web-chat-input{flex:1;min-width:0;padding:6px 8px;border:1px solid #1f2335;border-radius:6px;background:#1a1b26;color:#c0caf5;font:13px/1.3 inherit;}',
|
|
682
|
+
'.web-chat-input:focus{outline:none;border-color:#7aa2f7;}',
|
|
683
|
+
'.web-chat-send{padding:6px 14px;border:0;border-radius:6px;background:#7aa2f7;color:#0b0b14;font-weight:600;cursor:pointer;}',
|
|
684
|
+
'.web-chat-send:hover{background:#8fb3ff;}',
|
|
685
|
+
].join('');
|
|
686
|
+
document.head.appendChild(style);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Build the two WS frames that drive T2's server-side two-stage assembler
|
|
690
|
+
// (routeWebChatInput): (1) the prompt wrapped in bracketed-paste markers —
|
|
691
|
+
// buffered server-side after the markers are stripped — then (2) a lone CR,
|
|
692
|
+
// the submit signal that fires grok.inject on the accumulated text. The
|
|
693
|
+
// wrapper is what makes a MULTI-LINE prompt safe: the server submits on a
|
|
694
|
+
// TRAILING CR, so a naive `text + '\r'` would submit early on an embedded
|
|
695
|
+
// newline. Same shape as the 4+1 orchestrator inject. Pure (vm-extract tested).
|
|
696
|
+
function webChatSubmitFrames(text) {
|
|
697
|
+
return [
|
|
698
|
+
{ type: 'input', data: '\x1b[200~' + String(text) + '\x1b[201~' },
|
|
699
|
+
{ type: 'input', data: '\r' },
|
|
700
|
+
];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function mountWebChatPanel(id, sessionData, panel) {
|
|
704
|
+
const meta = sessionData.meta;
|
|
705
|
+
ensureWebChatStyles();
|
|
706
|
+
|
|
707
|
+
const container = document.getElementById(`term-${id}`);
|
|
708
|
+
container.classList.add('web-chat-terminal');
|
|
709
|
+
container.innerHTML =
|
|
710
|
+
`<div class="web-chat-stage" id="wc-stage-${id}">` +
|
|
711
|
+
`<canvas class="web-chat-canvas" id="wc-canvas-${id}" tabindex="0"></canvas>` +
|
|
712
|
+
`</div>` +
|
|
713
|
+
`<form class="web-chat-input-bar" id="wc-form-${id}" autocomplete="off">` +
|
|
714
|
+
`<input type="text" class="web-chat-input" id="wc-input-${id}" placeholder="Message Grok… (Enter sends into the live session)">` +
|
|
715
|
+
`<button type="submit" class="web-chat-send" id="wc-send-${id}">send</button>` +
|
|
716
|
+
`</form>`;
|
|
717
|
+
|
|
718
|
+
const stage = document.getElementById(`wc-stage-${id}`);
|
|
719
|
+
const canvas = document.getElementById(`wc-canvas-${id}`);
|
|
720
|
+
const ctx = canvas.getContext('2d');
|
|
721
|
+
const inputEl = document.getElementById(`wc-input-${id}`);
|
|
722
|
+
const formEl = document.getElementById(`wc-form-${id}`);
|
|
723
|
+
|
|
724
|
+
let lastImg = null, lastDevW = 0, lastDevH = 0, map = null;
|
|
725
|
+
|
|
726
|
+
// Paint one screencast frame. T1's frame-channel shape exposes `dataUrl`
|
|
727
|
+
// (drops straight into Image.src) + deviceWidth/deviceHeight (the page
|
|
728
|
+
// viewport in CSS px, needed to map clicks back to page coordinates).
|
|
729
|
+
function paintFrame(frame) {
|
|
730
|
+
if (!frame) return;
|
|
731
|
+
const url = frame.dataUrl || (frame.data ? `data:image/${frame.format || 'jpeg'};base64,${frame.data}` : null);
|
|
732
|
+
if (!url) return;
|
|
733
|
+
const img = new Image();
|
|
734
|
+
img.onload = () => {
|
|
735
|
+
lastImg = img;
|
|
736
|
+
lastDevW = frame.deviceWidth || img.naturalWidth;
|
|
737
|
+
lastDevH = frame.deviceHeight || img.naturalHeight;
|
|
738
|
+
drawLastFrame();
|
|
739
|
+
};
|
|
740
|
+
img.src = url;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Letterbox the last frame into the canvas, tracking the transform so
|
|
744
|
+
// pointer events can be mapped back to page coordinates.
|
|
745
|
+
function drawLastFrame() {
|
|
746
|
+
if (!lastImg) return;
|
|
747
|
+
const rect = stage.getBoundingClientRect();
|
|
748
|
+
const dpr = window.devicePixelRatio || 1;
|
|
749
|
+
const cw = Math.max(1, Math.round(rect.width)), ch = Math.max(1, Math.round(rect.height));
|
|
750
|
+
if (canvas.width !== Math.round(cw * dpr) || canvas.height !== Math.round(ch * dpr)) {
|
|
751
|
+
canvas.width = Math.round(cw * dpr);
|
|
752
|
+
canvas.height = Math.round(ch * dpr);
|
|
753
|
+
canvas.style.width = cw + 'px';
|
|
754
|
+
canvas.style.height = ch + 'px';
|
|
755
|
+
}
|
|
756
|
+
const iw = lastImg.naturalWidth, ih = lastImg.naturalHeight;
|
|
757
|
+
const scale = Math.min(canvas.width / iw, canvas.height / ih);
|
|
758
|
+
const dw = iw * scale, dh = ih * scale;
|
|
759
|
+
const dx = (canvas.width - dw) / 2, dy = (canvas.height - dh) / 2;
|
|
760
|
+
ctx.fillStyle = '#000';
|
|
761
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
762
|
+
ctx.drawImage(lastImg, dx, dy, dw, dh);
|
|
763
|
+
map = { dx, dy, scale, iw, ih, dpr };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// canvas client px → page CSS px (or null if the click hit the letterbox).
|
|
767
|
+
function mapToPage(clientX, clientY) {
|
|
768
|
+
if (!map || !lastDevW || !lastDevH) return null;
|
|
769
|
+
const rect = canvas.getBoundingClientRect();
|
|
770
|
+
const px = (clientX - rect.left) * map.dpr, py = (clientY - rect.top) * map.dpr;
|
|
771
|
+
const ix = (px - map.dx) / map.scale, iy = (py - map.dy) / map.scale;
|
|
772
|
+
if (ix < 0 || iy < 0 || ix > map.iw || iy > map.ih) return null;
|
|
773
|
+
return { x: Math.round(ix * (lastDevW / map.iw)), y: Math.round(iy * (lastDevH / map.ih)) };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function liveWs() { const e = state.sessions.get(id); return e && e.ws; }
|
|
777
|
+
// Submit a composer prompt via T2's two-stage assembler (bracketed-paste
|
|
778
|
+
// body buffered, then a lone CR fires grok.inject). NOT a single raw
|
|
779
|
+
// {type:'input',data:text} — that has no submit sentinel and buffers
|
|
780
|
+
// server-side forever (T4 FINDING 13:30).
|
|
781
|
+
function sendComposerSubmit(text) {
|
|
782
|
+
const ws = liveWs();
|
|
783
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
784
|
+
for (const frame of webChatSubmitFrames(text)) ws.send(JSON.stringify(frame));
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
function sendCdpEvent(event) {
|
|
788
|
+
const ws = liveWs();
|
|
789
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'web-chat-input', event }));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Input box → submit the prompt into the live composer via the two-stage
|
|
793
|
+
// assembler. Clearing the field is gated on the WS send so a prompt is
|
|
794
|
+
// never silently lost when the socket is down.
|
|
795
|
+
formEl.addEventListener('submit', (e) => {
|
|
796
|
+
e.preventDefault();
|
|
797
|
+
const text = inputEl.value;
|
|
798
|
+
if (!text.trim()) return;
|
|
799
|
+
if (sendComposerSubmit(text)) inputEl.value = '';
|
|
800
|
+
});
|
|
801
|
+
inputEl.addEventListener('focus', () => { panel.classList.add('active-input'); state.focusedId = id; });
|
|
802
|
+
inputEl.addEventListener('blur', () => { panel.classList.remove('active-input'); });
|
|
803
|
+
|
|
804
|
+
// Canvas → forward clicks + wheel to the live tab so the human can drive
|
|
805
|
+
// it in-deck. (Text typing stays in the input box for v1; full keyboard
|
|
806
|
+
// forwarding is a future enhancement to avoid hijacking deck shortcuts.)
|
|
807
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
808
|
+
const pt = mapToPage(e.clientX, e.clientY); if (!pt) return;
|
|
809
|
+
canvas.focus(); state.focusedId = id;
|
|
810
|
+
sendCdpEvent({ kind: 'mouse', type: 'mousePressed', x: pt.x, y: pt.y, button: 'left', clickCount: 1 });
|
|
811
|
+
});
|
|
812
|
+
canvas.addEventListener('mouseup', (e) => {
|
|
813
|
+
const pt = mapToPage(e.clientX, e.clientY); if (!pt) return;
|
|
814
|
+
sendCdpEvent({ kind: 'mouse', type: 'mouseReleased', x: pt.x, y: pt.y, button: 'left', clickCount: 1 });
|
|
815
|
+
});
|
|
816
|
+
canvas.addEventListener('wheel', (e) => {
|
|
817
|
+
const pt = mapToPage(e.clientX, e.clientY); if (!pt) return;
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
sendCdpEvent({ kind: 'mouse', type: 'mouseWheel', x: pt.x, y: pt.y, deltaX: e.deltaX, deltaY: e.deltaY });
|
|
820
|
+
}, { passive: false });
|
|
821
|
+
|
|
822
|
+
const ws = new WebSocket(`${WS_BASE}?session=${id}`);
|
|
823
|
+
ws.onmessage = (event) => handleWebChatWsMessage(id, event, paintFrame);
|
|
824
|
+
ws.onclose = (event) => {
|
|
825
|
+
const e2 = state.sessions.get(id); if (!e2) return;
|
|
826
|
+
if (event.code === 4000 || event.code === 4001) return;
|
|
827
|
+
const p = document.getElementById(`panel-${id}`);
|
|
828
|
+
if (p && p.classList.contains('exited')) return;
|
|
829
|
+
const delay = Math.min(1000 * Math.pow(2, (e2._reconnectAttempts || 0)), 10000);
|
|
830
|
+
e2._reconnectAttempts = (e2._reconnectAttempts || 0) + 1;
|
|
831
|
+
if (e2._reconnectAttempts <= 5) setTimeout(() => reconnectWebChat(id), delay);
|
|
832
|
+
else updatePanelMeta(id, { status: 'errored', statusDetail: 'Connection lost' });
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// Re-letterbox on panel resize (fitAll() also calls our fitAddon.fit()).
|
|
836
|
+
const resizeObserver = new ResizeObserver(() => { try { drawLastFrame(); } catch (err) { /* ignore */ } });
|
|
837
|
+
resizeObserver.observe(stage);
|
|
838
|
+
|
|
839
|
+
state.sessions.set(id, {
|
|
840
|
+
session: sessionData,
|
|
841
|
+
ws,
|
|
842
|
+
el: panel,
|
|
843
|
+
isWebChat: true,
|
|
844
|
+
canvas, ctx, inputEl,
|
|
845
|
+
paintFrame, drawLastFrame,
|
|
846
|
+
// No xterm. A no-op-ish fitAddon keeps fitAll()/drawer-resize loops safe
|
|
847
|
+
// AND re-letterboxes the canvas on a global re-fit.
|
|
848
|
+
fitAddon: { fit() { drawLastFrame(); } },
|
|
849
|
+
activeTab: 'overview',
|
|
850
|
+
drawerOpen: false,
|
|
851
|
+
commandHistory: [],
|
|
852
|
+
commandsLoaded: false,
|
|
853
|
+
memoryHits: [],
|
|
854
|
+
statusLog: [],
|
|
855
|
+
webChatLog: [],
|
|
856
|
+
lastKnownStatus: meta.status,
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
appendStatusLog(id, meta.status, meta.statusDetail || '');
|
|
860
|
+
setupDrawerListeners(id);
|
|
861
|
+
renderOverviewTab(id);
|
|
862
|
+
renderSwitcher();
|
|
863
|
+
const replyBtn = document.getElementById(`reply-btn-${id}`);
|
|
864
|
+
if (replyBtn) replyBtn.disabled = state.sessions.size < 2;
|
|
865
|
+
refreshAllReplyFormsFor(id);
|
|
866
|
+
refreshPanelIndices();
|
|
867
|
+
revealNewPanelIfFiltered(meta);
|
|
868
|
+
refreshDashboardChrome();
|
|
869
|
+
requestAnimationFrame(() => drawLastFrame());
|
|
870
|
+
|
|
871
|
+
return { ws, canvas };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Shared WS dispatch for web-chat panels (used by both initial mount and
|
|
875
|
+
// reconnect). Mirrors the xterm handler's non-output cases exactly (same
|
|
876
|
+
// downstream fns) but routes screencast frames → canvas and grok text → an
|
|
877
|
+
// in-memory transcript instead of writing to an xterm.
|
|
878
|
+
function handleWebChatWsMessage(id, event, paintFrame) {
|
|
879
|
+
let msg;
|
|
880
|
+
try { msg = JSON.parse(event.data); }
|
|
881
|
+
catch (err) { console.error('[client] web-chat ws parse failed:', err); return; }
|
|
882
|
+
const entry = state.sessions.get(id);
|
|
883
|
+
switch (msg.type) {
|
|
884
|
+
case 'web-chat-frame':
|
|
885
|
+
if (paintFrame) paintFrame(msg.frame);
|
|
886
|
+
else if (entry && entry.paintFrame) entry.paintFrame(msg.frame);
|
|
887
|
+
break;
|
|
888
|
+
case 'output':
|
|
889
|
+
// Grok response text. The canvas is the visual; keep a transcript for
|
|
890
|
+
// potential future drawer rendering. Never write to a (nonexistent) xterm.
|
|
891
|
+
if (entry) { (entry.webChatLog = entry.webChatLog || []).push(msg.data); }
|
|
892
|
+
break;
|
|
893
|
+
case 'meta':
|
|
894
|
+
updatePanelMeta(id, msg.session.meta);
|
|
895
|
+
break;
|
|
896
|
+
case 'proactive_memory':
|
|
897
|
+
showProactiveToast(id, msg.hit, msg.flashback_event_id);
|
|
898
|
+
break;
|
|
899
|
+
case 'exit': {
|
|
900
|
+
updatePanelMeta(id, { status: 'exited', statusDetail: `Exited (${msg.exitCode})` });
|
|
901
|
+
const p = document.getElementById(`panel-${id}`);
|
|
902
|
+
if (p) p.classList.add('exited');
|
|
903
|
+
refreshAllReplyFormsFor(id);
|
|
904
|
+
refreshPanelIndices();
|
|
905
|
+
renderSwitcher();
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
case 'panel_exited':
|
|
909
|
+
handlePanelExited(msg.sessionId, msg.exitCode);
|
|
910
|
+
break;
|
|
911
|
+
case 'status_broadcast':
|
|
912
|
+
updateGlobalStats(msg.sessions);
|
|
913
|
+
break;
|
|
914
|
+
case 'config_changed':
|
|
915
|
+
if (msg.config) {
|
|
916
|
+
state.config = { ...state.config, ...msg.config };
|
|
917
|
+
if (typeof renderSettingsPanel === 'function') renderSettingsPanel();
|
|
918
|
+
if (typeof updateRagIndicator === 'function') updateRagIndicator();
|
|
919
|
+
}
|
|
920
|
+
break;
|
|
921
|
+
case 'projects_changed':
|
|
922
|
+
if (msg.projects && state.config) {
|
|
923
|
+
state.config.projects = msg.projects;
|
|
924
|
+
if (typeof rebuildProjectDropdown === 'function') rebuildProjectDropdown();
|
|
925
|
+
}
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function reconnectWebChat(id) {
|
|
931
|
+
const entry = state.sessions.get(id);
|
|
932
|
+
if (!entry) return;
|
|
933
|
+
const ws = new WebSocket(`${WS_BASE}?session=${id}`);
|
|
934
|
+
ws.onmessage = (event) => handleWebChatWsMessage(id, event, entry.paintFrame);
|
|
935
|
+
ws.onopen = () => {
|
|
936
|
+
entry._reconnectAttempts = 0;
|
|
937
|
+
entry.ws = ws;
|
|
938
|
+
updatePanelMeta(id, { status: 'active', statusDetail: 'Reconnected' });
|
|
939
|
+
};
|
|
940
|
+
ws.onclose = (event) => {
|
|
941
|
+
const p = document.getElementById(`panel-${id}`);
|
|
942
|
+
if (p && p.classList.contains('exited')) return;
|
|
943
|
+
if (event.code === 4001) {
|
|
944
|
+
updatePanelMeta(id, { status: 'exited', statusDetail: 'Session ended' });
|
|
945
|
+
if (p) p.classList.add('exited');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const delay = Math.min(1000 * Math.pow(2, (entry._reconnectAttempts || 0)), 10000);
|
|
949
|
+
entry._reconnectAttempts = (entry._reconnectAttempts || 0) + 1;
|
|
950
|
+
if (entry._reconnectAttempts <= 5) setTimeout(() => reconnectWebChat(id), delay);
|
|
951
|
+
else updatePanelMeta(id, { status: 'errored', statusDetail: 'Connection lost' });
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
656
955
|
// ===== Sprint 65: project-filter chips + ORCH-panel pin + tile lifecycle =====
|
|
657
956
|
// Brad's 2026-05-13 v2 spec (BACKLOG § D.5) — three dashboard-reliability
|
|
658
957
|
// surfaces sharing one client-side lane:
|
|
@@ -1738,8 +2037,10 @@
|
|
|
1738
2037
|
entry.el.classList.add('primary');
|
|
1739
2038
|
}
|
|
1740
2039
|
|
|
1741
|
-
// Focus the xterm textarea (without stealing pointer)
|
|
1742
|
-
|
|
2040
|
+
// Focus the xterm textarea (without stealing pointer); web-chat panels
|
|
2041
|
+
// focus their inject input instead.
|
|
2042
|
+
if (entry.terminal) { try { entry.terminal.focus(); } catch (err) { /* ignore */ } }
|
|
2043
|
+
else if (entry.inputEl) { try { entry.inputEl.focus(); } catch (err) { /* ignore */ } }
|
|
1743
2044
|
state.focusedId = id;
|
|
1744
2045
|
|
|
1745
2046
|
// Flash the panel border briefly
|
|
@@ -1833,7 +2134,7 @@
|
|
|
1833
2134
|
setTimeout(() => {
|
|
1834
2135
|
try { entry.fitAddon.fit(); } catch (err) { /* ignore */ }
|
|
1835
2136
|
const ws = entry.ws;
|
|
1836
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2137
|
+
if (ws && ws.readyState === WebSocket.OPEN && entry.terminal) { // PTY resize only; web-chat has no cols/rows
|
|
1837
2138
|
ws.send(JSON.stringify({
|
|
1838
2139
|
type: 'resize',
|
|
1839
2140
|
cols: entry.terminal.cols,
|
|
@@ -2054,6 +2355,8 @@
|
|
|
2054
2355
|
const entry = state.sessions.get(id);
|
|
2055
2356
|
if (entry && entry.terminal) {
|
|
2056
2357
|
try { entry.terminal.focus(); } catch (err) { /* ignore */ }
|
|
2358
|
+
} else if (entry && entry.inputEl) {
|
|
2359
|
+
try { entry.inputEl.focus(); } catch (err) { /* ignore */ } // web-chat inject box
|
|
2057
2360
|
}
|
|
2058
2361
|
|
|
2059
2362
|
// Re-fit all visible terminals
|
|
@@ -2164,9 +2467,9 @@
|
|
|
2164
2467
|
|
|
2165
2468
|
const entry = state.sessions.get(id);
|
|
2166
2469
|
if (entry) {
|
|
2167
|
-
entry.terminal.dispose();
|
|
2168
|
-
entry.ws.close();
|
|
2169
|
-
entry.el.remove();
|
|
2470
|
+
if (entry.terminal) entry.terminal.dispose(); // web-chat panels have no xterm
|
|
2471
|
+
if (entry.ws) entry.ws.close();
|
|
2472
|
+
if (entry.el) entry.el.remove();
|
|
2170
2473
|
state.sessions.delete(id);
|
|
2171
2474
|
}
|
|
2172
2475
|
|
|
@@ -2183,7 +2486,7 @@
|
|
|
2183
2486
|
if (!entry) return;
|
|
2184
2487
|
|
|
2185
2488
|
const themeObj = getThemeObject(themeId);
|
|
2186
|
-
entry.terminal.options.theme = themeObj;
|
|
2489
|
+
if (entry.terminal) entry.terminal.options.theme = themeObj; // no xterm on web-chat panels
|
|
2187
2490
|
|
|
2188
2491
|
// Persist to server (writes to sessions.theme_override server-side)
|
|
2189
2492
|
api('PATCH', `/api/sessions/${id}`, { theme: themeId });
|
|
@@ -2199,7 +2502,7 @@
|
|
|
2199
2502
|
const updated = await api('PATCH', '/api/sessions/' + id, { theme: null });
|
|
2200
2503
|
const resolved = updated && updated.meta && updated.meta.theme;
|
|
2201
2504
|
if (!resolved) return;
|
|
2202
|
-
entry.terminal.options.theme = getThemeObject(resolved);
|
|
2505
|
+
if (entry.terminal) entry.terminal.options.theme = getThemeObject(resolved);
|
|
2203
2506
|
const sel = document.getElementById('theme-' + id);
|
|
2204
2507
|
if (sel && sel.value !== resolved) sel.value = resolved;
|
|
2205
2508
|
}
|
|
@@ -2267,9 +2570,14 @@
|
|
|
2267
2570
|
const entry = state.sessions.get(id);
|
|
2268
2571
|
if (!entry) return;
|
|
2269
2572
|
|
|
2573
|
+
// Sprint 72 T3 — web-chat panels have no xterm; route mnestra output to a
|
|
2574
|
+
// no-op writer so askAI still caches Memory-tab hits without crashing on a
|
|
2575
|
+
// missing terminal. (xterm panels render inline exactly as before.)
|
|
2576
|
+
const tw = (s) => { if (entry.terminal) { try { entry.terminal.write(s); } catch (e) { /* ignore */ } } };
|
|
2577
|
+
|
|
2270
2578
|
// Early return if AI queries are not available
|
|
2271
2579
|
if (!state.config.aiQueryAvailable) {
|
|
2272
|
-
|
|
2580
|
+
tw(
|
|
2273
2581
|
'\r\n\x1b[33m[mnestra] AI queries are not available.\x1b[0m\r\n' +
|
|
2274
2582
|
'\x1b[33mTo enable, add the following to ~/.termdeck/config.yaml:\x1b[0m\r\n' +
|
|
2275
2583
|
'\x1b[90m rag:\r\n' +
|
|
@@ -2292,7 +2600,7 @@
|
|
|
2292
2600
|
});
|
|
2293
2601
|
|
|
2294
2602
|
if (result.error) {
|
|
2295
|
-
|
|
2603
|
+
tw(`\r\n\x1b[33m[mnestra] ${result.error}\x1b[0m\r\n`);
|
|
2296
2604
|
} else if (result.memories && result.memories.length > 0) {
|
|
2297
2605
|
// Cache hits for the Memory tab
|
|
2298
2606
|
if (!entry.memoryHits) entry.memoryHits = [];
|
|
@@ -2307,7 +2615,7 @@
|
|
|
2307
2615
|
if (entry.drawerOpen && entry.activeTab === 'memory') {
|
|
2308
2616
|
renderMemoryTab(id);
|
|
2309
2617
|
}
|
|
2310
|
-
const cols = entry.terminal.cols || 80;
|
|
2618
|
+
const cols = (entry.terminal && entry.terminal.cols) || 80;
|
|
2311
2619
|
const wrap = (text, indent) => {
|
|
2312
2620
|
const maxW = cols - indent - 2;
|
|
2313
2621
|
const words = text.split(/\s+/);
|
|
@@ -2325,23 +2633,23 @@
|
|
|
2325
2633
|
return lines;
|
|
2326
2634
|
};
|
|
2327
2635
|
|
|
2328
|
-
|
|
2636
|
+
tw(`\r\n\x1b[36m━━━ Mnestra: ${result.total} memories found ━━━\x1b[0m\r\n`);
|
|
2329
2637
|
for (const m of result.memories) {
|
|
2330
2638
|
const score = m.similarity ? `${(m.similarity * 100).toFixed(0)}%` : '';
|
|
2331
2639
|
const proj = m.project ? m.project : '';
|
|
2332
|
-
|
|
2640
|
+
tw(`\r\n\x1b[35m● ${m.source_type}\x1b[0m \x1b[90m${proj} ${score}\x1b[0m\r\n`);
|
|
2333
2641
|
const contentLines = wrap(m.content || '(empty)', 2);
|
|
2334
2642
|
for (const cl of contentLines) {
|
|
2335
|
-
|
|
2643
|
+
tw(`${cl}\r\n`);
|
|
2336
2644
|
}
|
|
2337
2645
|
}
|
|
2338
|
-
|
|
2646
|
+
tw(`\r\n\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\r\n\r\n`);
|
|
2339
2647
|
} else {
|
|
2340
|
-
|
|
2648
|
+
tw(`\r\n\x1b[33m[mnestra] No relevant memories found.\x1b[0m\r\n`);
|
|
2341
2649
|
}
|
|
2342
2650
|
} catch (err) {
|
|
2343
2651
|
console.error('[client] AI query failed:', err);
|
|
2344
|
-
|
|
2652
|
+
tw(`\r\n\x1b[31m[mnestra] Query failed: ${err.message}\x1b[0m\r\n`);
|
|
2345
2653
|
}
|
|
2346
2654
|
|
|
2347
2655
|
inputEl.value = '';
|
|
@@ -3377,6 +3685,12 @@
|
|
|
3377
3685
|
|
|
3378
3686
|
// ===== Layout =====
|
|
3379
3687
|
function setLayout(layout) {
|
|
3688
|
+
// Sprint 67 T3: legacy `orch` layout retired (superseded by the role-tagged
|
|
3689
|
+
// ORCH-pin row from Sprint 65). Redirect any stale callers to `4x2` so the
|
|
3690
|
+
// grid still renders cleanly if `orch` arrives from older code paths.
|
|
3691
|
+
if (layout === 'orch') {
|
|
3692
|
+
layout = '4x2';
|
|
3693
|
+
}
|
|
3380
3694
|
const wasControl = state.layout === 'control';
|
|
3381
3695
|
// Only persist "real" grid layouts as state.layout; the control view is
|
|
3382
3696
|
// an overlay, not a target to restore to when the user hits Escape.
|
|
@@ -3385,15 +3699,7 @@
|
|
|
3385
3699
|
}
|
|
3386
3700
|
const grid = document.getElementById('termGrid');
|
|
3387
3701
|
grid.className = `grid-container layout-${layout}`;
|
|
3388
|
-
|
|
3389
|
-
// Orchestrator layout: set column count based on worker panels (total - 1)
|
|
3390
|
-
if (layout === 'orch') {
|
|
3391
|
-
const panelCount = grid.querySelectorAll('.term-panel').length;
|
|
3392
|
-
const workerCount = Math.max(0, panelCount - 1);
|
|
3393
|
-
grid.setAttribute('data-orch-cols', String(workerCount || panelCount));
|
|
3394
|
-
} else {
|
|
3395
|
-
grid.removeAttribute('data-orch-cols');
|
|
3396
|
-
}
|
|
3702
|
+
grid.removeAttribute('data-orch-cols');
|
|
3397
3703
|
|
|
3398
3704
|
// Remove focus/half states
|
|
3399
3705
|
document.querySelectorAll('.term-panel').forEach(p => {
|
|
@@ -3449,6 +3755,7 @@
|
|
|
3449
3755
|
'codex': 'Codex CLI',
|
|
3450
3756
|
'gemini': 'Gemini CLI',
|
|
3451
3757
|
'grok': 'Grok CLI',
|
|
3758
|
+
'web-chat': 'Grok (web)',
|
|
3452
3759
|
'python-server': 'Python Server',
|
|
3453
3760
|
'one-shot': 'One-shot'
|
|
3454
3761
|
};
|
|
@@ -3544,7 +3851,7 @@
|
|
|
3544
3851
|
if (themeSelect && themeSelect.value !== meta.theme) {
|
|
3545
3852
|
themeSelect.value = meta.theme;
|
|
3546
3853
|
const entry = state.sessions.get(id);
|
|
3547
|
-
if (entry) {
|
|
3854
|
+
if (entry && entry.terminal) { // web-chat panels have no xterm to theme
|
|
3548
3855
|
entry.terminal.options.theme = getThemeObject(meta.theme);
|
|
3549
3856
|
}
|
|
3550
3857
|
}
|
|
@@ -3685,7 +3992,7 @@
|
|
|
3685
3992
|
{
|
|
3686
3993
|
target: '.topbar-center',
|
|
3687
3994
|
title: 'Layout modes',
|
|
3688
|
-
body: `
|
|
3995
|
+
body: `Preset grid layouts — <kbd>1x1</kbd> through <kbd>4x4</kbd> — plus <strong>control</strong> (aggregate activity feed). Click any layout to switch instantly; all terminals re-fit to the new grid. Keyboard shortcuts <kbd>Cmd+Shift+1</kbd>–<kbd>Cmd+Shift+9</kbd> (or <kbd>Ctrl+Shift+1</kbd>–<kbd>9</kbd>) cycle through them. Orchestrator panels are pinned to a dedicated row above the grid via <strong>meta.role</strong>; no special "orch" layout needed.`,
|
|
3689
3996
|
},
|
|
3690
3997
|
{
|
|
3691
3998
|
target: '#termSwitcher',
|
|
@@ -5001,7 +5308,10 @@
|
|
|
5001
5308
|
// the new dense presets. Topbar buttons cover every preset incl. 2x5/5x2/3x4.
|
|
5002
5309
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key >= '0' && e.key <= '9') {
|
|
5003
5310
|
e.preventDefault();
|
|
5004
|
-
|
|
5311
|
+
// Sprint 67 T3: index 6 (was `orch`, key 7) is now null — the legacy
|
|
5312
|
+
// orch layout is retired in favor of the role-tagged ORCH-pin row.
|
|
5313
|
+
// Keep the slot to preserve muscle memory on keys 8/9/0.
|
|
5314
|
+
const layouts = ['1x1', '2x1', '2x2', '3x2', '2x4', '4x2', null, '1x2', '4x3', '4x4'];
|
|
5005
5315
|
const idx = e.key === '0' ? 9 : parseInt(e.key, 10) - 1;
|
|
5006
5316
|
if (layouts[idx]) setLayout(layouts[idx]);
|
|
5007
5317
|
}
|
|
@@ -5016,7 +5326,8 @@
|
|
|
5016
5326
|
: (curIdx - 1 + ids.length) % ids.length;
|
|
5017
5327
|
const entry = state.sessions.get(ids[next]);
|
|
5018
5328
|
if (entry) {
|
|
5019
|
-
entry.terminal.focus();
|
|
5329
|
+
if (entry.terminal) entry.terminal.focus();
|
|
5330
|
+
else if (entry.inputEl) entry.inputEl.focus(); // web-chat: focus the inject box
|
|
5020
5331
|
state.focusedId = ids[next];
|
|
5021
5332
|
}
|
|
5022
5333
|
}
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
<button class="layout-btn" data-layout="4x3" title="12 panels — 4 cols × 3 rows">4x3</button>
|
|
48
48
|
<button class="layout-btn" data-layout="3x4" title="12 panels — 3 cols × 4 rows">3x4</button>
|
|
49
49
|
<button class="layout-btn" data-layout="4x4" title="16 panels">4x4</button>
|
|
50
|
-
<button class="layout-btn" data-layout="orch" title="Orchestrator: 4 workers across top, 1 full-width orchestrator across bottom">orch</button>
|
|
51
50
|
<button class="layout-btn control-btn" data-layout="control" title="Aggregate activity feed">control</button>
|
|
52
51
|
</div>
|
|
53
52
|
</div>
|
|
@@ -329,27 +329,6 @@
|
|
|
329
329
|
.grid-container.layout-3x4 { grid-template-columns: 1fr 1fr 1fr; grid-template-rows: repeat(4, 1fr); }
|
|
330
330
|
.grid-container.layout-4x4 { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); }
|
|
331
331
|
|
|
332
|
-
/* Orchestrator: workers across the top (60%), one full-width orchestrator
|
|
333
|
-
panel across the bottom (40%). The last panel is always the orchestrator.
|
|
334
|
-
JS sets a data-orch-cols attribute on the grid to match the worker count. */
|
|
335
|
-
/* Orchestrator: 2x2 workers on top (60%), full-width orchestrator bottom (40%) */
|
|
336
|
-
.grid-container.layout-orch {
|
|
337
|
-
grid-template-columns: repeat(2, 1fr);
|
|
338
|
-
grid-template-rows: 2fr 2fr 2fr;
|
|
339
|
-
}
|
|
340
|
-
.grid-container.layout-orch .term-panel:last-child {
|
|
341
|
-
grid-column: 1 / -1;
|
|
342
|
-
grid-row: 3;
|
|
343
|
-
}
|
|
344
|
-
/* Single panel: fill entire grid */
|
|
345
|
-
.grid-container.layout-orch[data-orch-cols="0"] {
|
|
346
|
-
grid-template-rows: 1fr;
|
|
347
|
-
grid-template-columns: 1fr;
|
|
348
|
-
}
|
|
349
|
-
.grid-container.layout-orch[data-orch-cols="0"] .term-panel:last-child {
|
|
350
|
-
grid-row: 1;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
332
|
/* Focus mode: single terminal fills the grid */
|
|
354
333
|
.grid-container.layout-focus { grid-template-columns: 1fr; grid-template-rows: 1fr; }
|
|
355
334
|
.grid-container.layout-focus .term-panel:not(.focused) { display: none; }
|
|
@@ -3036,17 +3015,9 @@
|
|
|
3036
3015
|
}
|
|
3037
3016
|
}
|
|
3038
3017
|
|
|
3039
|
-
/* Very narrow viewports (rare — sub-1024 widths):
|
|
3040
|
-
|
|
3041
|
-
let it scroll; orch falls back to a single-column stack. */
|
|
3018
|
+
/* Very narrow viewports (rare — sub-1024 widths): let dense grids scroll
|
|
3019
|
+
rather than truncate panels. */
|
|
3042
3020
|
@media (max-width: 900px) {
|
|
3043
|
-
.grid-container.layout-orch {
|
|
3044
|
-
grid-template-columns: 1fr;
|
|
3045
|
-
grid-template-rows: repeat(var(--orch-worker-rows, 3), 1fr) 1.2fr;
|
|
3046
|
-
}
|
|
3047
|
-
.grid-container.layout-orch .term-panel:last-child {
|
|
3048
|
-
grid-column: 1;
|
|
3049
|
-
}
|
|
3050
3021
|
.grid-container { overflow: auto; }
|
|
3051
3022
|
}
|
|
3052
3023
|
|