@jhizzard/termdeck 1.7.0 → 1.8.1
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 +4 -3
- package/packages/client/public/app.js +329 -19
- package/packages/server/src/agent-adapters/agy.js +21 -30
- package/packages/server/src/agent-adapters/index.js +12 -0
- package/packages/server/src/agent-adapters/web-chat-grok.js +259 -0
- package/packages/server/src/index.js +558 -8
- package/packages/server/src/sprints/status-parser.js +14 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
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"
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"packages/server",
|
|
26
26
|
"packages/client",
|
|
27
27
|
"packages/cli",
|
|
28
|
-
"packages/stack-installer"
|
|
28
|
+
"packages/stack-installer",
|
|
29
|
+
"packages/mcp-bridge"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
31
32
|
"dev": "node packages/server/src/index.js",
|
|
32
33
|
"server": "node packages/server/src/index.js",
|
|
33
34
|
"start": "NODE_ENV=production node packages/cli/src/index.js",
|
|
34
|
-
"test": "node --test packages/server/tests/**/*.test.js packages/cli/tests/**/*.test.js packages/stack-installer/tests/**/*.test.js",
|
|
35
|
+
"test": "WEB_CHAT_DRIVER_NO_BROWSER=1 node --test packages/server/tests/**/*.test.js packages/cli/tests/**/*.test.js packages/stack-installer/tests/**/*.test.js packages/mcp-bridge/test/*.test.js packages/web-chat-driver/tests/*.test.js",
|
|
35
36
|
"install:app": "bash install.sh",
|
|
36
37
|
"sync-rumen-functions": "bash scripts/sync-rumen-functions.sh",
|
|
37
38
|
"sync:agents": "node scripts/sync-agent-instructions.js"
|
|
@@ -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 = '';
|
|
@@ -3447,6 +3755,7 @@
|
|
|
3447
3755
|
'codex': 'Codex CLI',
|
|
3448
3756
|
'gemini': 'Gemini CLI',
|
|
3449
3757
|
'grok': 'Grok CLI',
|
|
3758
|
+
'web-chat': 'Grok (web)',
|
|
3450
3759
|
'python-server': 'Python Server',
|
|
3451
3760
|
'one-shot': 'One-shot'
|
|
3452
3761
|
};
|
|
@@ -3542,7 +3851,7 @@
|
|
|
3542
3851
|
if (themeSelect && themeSelect.value !== meta.theme) {
|
|
3543
3852
|
themeSelect.value = meta.theme;
|
|
3544
3853
|
const entry = state.sessions.get(id);
|
|
3545
|
-
if (entry) {
|
|
3854
|
+
if (entry && entry.terminal) { // web-chat panels have no xterm to theme
|
|
3546
3855
|
entry.terminal.options.theme = getThemeObject(meta.theme);
|
|
3547
3856
|
}
|
|
3548
3857
|
}
|
|
@@ -5017,7 +5326,8 @@
|
|
|
5017
5326
|
: (curIdx - 1 + ids.length) % ids.length;
|
|
5018
5327
|
const entry = state.sessions.get(ids[next]);
|
|
5019
5328
|
if (entry) {
|
|
5020
|
-
entry.terminal.focus();
|
|
5329
|
+
if (entry.terminal) entry.terminal.focus();
|
|
5330
|
+
else if (entry.inputEl) entry.inputEl.focus(); // web-chat: focus the inject box
|
|
5021
5331
|
state.focusedId = ids[next];
|
|
5022
5332
|
}
|
|
5023
5333
|
}
|
|
@@ -309,32 +309,25 @@ function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
// ──────────────────────────────────────────────────────────────────────────
|
|
312
|
-
// mcpConfig —
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
312
|
+
// mcpConfig — null (Mnestra MCP auto-wire intentionally OFF for agy).
|
|
313
|
+
// VERIFIED 2026-06-08 (live 4-CLI 360): Antigravity's MCP is NOT file-config-
|
|
314
|
+
// driven — agy's MCP servers are managed by its embedded "exa" language-server
|
|
315
|
+
// (RPCs `RefreshMcpServers` / `GetMcpServerStates`; type
|
|
316
|
+
// `gemini.GeminiMCPServerConfig`), not a readable `mcp_config.json`. Ruled out
|
|
317
|
+
// empirically against a LIVE agy panel: a de-secreted mnestra block written to
|
|
318
|
+
// BOTH `~/.gemini/config/mcp_config.json` AND the appDataDir
|
|
319
|
+
// `~/.gemini/antigravity-cli/mcp_config.json` left agy reporting
|
|
320
|
+
// `NO-MNESTRA-TOOL`; `~/.gemini/settings.json` already carries mnestra (gemini
|
|
321
|
+
// reads it) yet agy ignores it; `agy --help` exposes no `mcp` subcommand and
|
|
322
|
+
// `agy plugin list` is empty. A file-based mcpConfig here only targets a dead
|
|
323
|
+
// path, so it is `null` → the shared mcp-autowire helper cleanly skips (exactly
|
|
324
|
+
// the Claude case). Wiring Mnestra into agy is a deferred follow-up via the
|
|
325
|
+
// Antigravity language-server registration mechanism (likely IDE- /
|
|
326
|
+
// `RefreshMcpServers`-driven). This was always a non-load-bearing nicety: agy's
|
|
327
|
+
// PTY panel + the memory CAPTURE path (source_agent=antigravity, Sprint 70) both
|
|
328
|
+
// work; only the agy-side memory READ is deferred.
|
|
323
329
|
// ──────────────────────────────────────────────────────────────────────────
|
|
324
330
|
|
|
325
|
-
const MNESTRA_ENV_KEYS = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'OPENAI_API_KEY'];
|
|
326
|
-
|
|
327
|
-
function buildMnestraBlock({ secrets } = {}) {
|
|
328
|
-
const env = {};
|
|
329
|
-
for (const key of MNESTRA_ENV_KEYS) {
|
|
330
|
-
const value = secrets && secrets[key];
|
|
331
|
-
if (typeof value === 'string' && value.length > 0 && !/^\$\{[^}]*\}$/.test(value)) {
|
|
332
|
-
env[key] = value;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return { mnestra: { command: 'mnestra', args: [], env } };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
331
|
const antigravityAdapter = {
|
|
339
332
|
name: 'antigravity',
|
|
340
333
|
sessionType: 'antigravity',
|
|
@@ -385,12 +378,10 @@ const antigravityAdapter = {
|
|
|
385
378
|
// true (bracketed-paste fast path), flip to false if a lane-time test shows
|
|
386
379
|
// the TUI input box eats the paste markers.
|
|
387
380
|
acceptsPaste: true,
|
|
388
|
-
mcpConfig
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
mnestraBlock: buildMnestraBlock,
|
|
393
|
-
},
|
|
381
|
+
// See the mcpConfig note above — Antigravity MCP is language-server-mediated,
|
|
382
|
+
// not file-config; null so mcp-autowire skips (Claude-style) instead of writing
|
|
383
|
+
// a dead-path file. agy memory READ is a deferred follow-up; CAPTURE works.
|
|
384
|
+
mcpConfig: null,
|
|
394
385
|
};
|
|
395
386
|
|
|
396
387
|
module.exports = antigravityAdapter;
|
|
@@ -19,6 +19,12 @@ const grok = require('./grok');
|
|
|
19
19
|
// Sprint 70 T1 — Antigravity CLI (`agy`). Registered under its canonical
|
|
20
20
|
// adapter name `antigravity` (= source_agent); the binary it matches is `agy`.
|
|
21
21
|
const antigravity = require('./agy');
|
|
22
|
+
// Sprint 72 T2 — Grok web-chat panel (`type:'web-chat'`). NOT a node-pty agent:
|
|
23
|
+
// driven by the CDP render-bridge (packages/web-chat-driver) against a real
|
|
24
|
+
// grok.com tab. Registered for `getAdapterForSessionType('web-chat')` +
|
|
25
|
+
// onPanelClose/periodic capture only — its `matches:()=>false` + absent
|
|
26
|
+
// `patterns.prompt` mean it never participates in output/command detection.
|
|
27
|
+
const webChatGrok = require('./web-chat-grok');
|
|
22
28
|
|
|
23
29
|
// Keyed by adapter name (NOT session.meta.type — adapters expose their own
|
|
24
30
|
// `sessionType` field for that mapping). Order is iteration order for the
|
|
@@ -32,6 +38,12 @@ const AGENT_ADAPTERS = {
|
|
|
32
38
|
// claims that string in the detect loop. agy panels are normally resolved by
|
|
33
39
|
// exact-binary direct-spawn, not output sniffing, so order is not load-bearing.
|
|
34
40
|
antigravity,
|
|
41
|
+
// Sprint 72 T2 — web-chat-grok. Order is irrelevant: it carries no
|
|
42
|
+
// `patterns.prompt` and `matches()` returns false, so detectAdapter() + the
|
|
43
|
+
// direct-spawn loop skip it entirely. It is reachable ONLY via
|
|
44
|
+
// getAdapterForSessionType('web-chat') (the `.find(a=>a.sessionType===type)`
|
|
45
|
+
// fallback, since the registry key 'web-chat-grok' ≠ sessionType 'web-chat').
|
|
46
|
+
'web-chat-grok': webChatGrok,
|
|
35
47
|
};
|
|
36
48
|
|
|
37
49
|
// Convenience accessor — returns the adapter whose `sessionType` matches the
|