Skip to content

Commit 3aa43fe

Browse files
committed
fix: shared terminals issues
1 parent 6852018 commit 3aa43fe

4 files changed

Lines changed: 80 additions & 65 deletions

File tree

app/components/TerminalPanel.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ watch(isSharing, (sharing) => {
121121
sessions.value = [];
122122
splitId.value = null;
123123
savedPairsMap.clear();
124+
// Re-ensure a session in case the panel is already visible.
125+
// sharedTerminals watcher (immediate) will populate sessions if there are existing
126+
// terminals; ensureSession only creates a new local one if nothing else does.
127+
nextTick(() => ensureSession());
124128
}
125129
});
126130

app/composables/useShare.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,17 @@ export function useShare() {
384384
}
385385

386386
// --- Event callbacks (set by index.vue or other consumers) ---
387-
export let onShareClosed: (() => void) | null = null;
388-
export let onRelayTerminalCreate: ((msg: any) => void) | null = null;
389-
export let onRelayTerminalInput: ((msg: any) => void) | null = null;
390-
export let onRelayTerminalResize: ((msg: any) => void) | null = null;
391-
export let onRelayTerminalClose: ((msg: any) => void) | null = null;
387+
// Use module-internal variables + setter functions.
388+
// Direct assignment to ES module namespace objects is read-only in production builds,
389+
// so we expose setters that mutate the internal bindings.
390+
let onShareClosed: (() => void) | null = null;
391+
let onRelayTerminalCreate: ((msg: any) => void) | null = null;
392+
let onRelayTerminalInput: ((msg: any) => void) | null = null;
393+
let onRelayTerminalResize: ((msg: any) => void) | null = null;
394+
let onRelayTerminalClose: ((msg: any) => void) | null = null;
395+
396+
export function setOnShareClosed(cb: (() => void) | null) { onShareClosed = cb; }
397+
export function setOnRelayTerminalCreate(cb: ((msg: any) => void) | null) { onRelayTerminalCreate = cb; }
398+
export function setOnRelayTerminalInput(cb: ((msg: any) => void) | null) { onRelayTerminalInput = cb; }
399+
export function setOnRelayTerminalResize(cb: ((msg: any) => void) | null) { onRelayTerminalResize = cb; }
400+
export function setOnRelayTerminalClose(cb: ((msg: any) => void) | null) { onRelayTerminalClose = cb; }

app/pages/index.vue

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
<div class="header-actions">
4545
<button @click="saveActivePane" class="btn"
4646
:class="{ 'btn-press': savePressing, 'btn-success': saveSuccess }"
47-
:disabled="!activePane?.filePath">Save</button>
47+
:disabled="!activePane?.filePath">
48+
{{ saveSuccess ? 'Saved' : 'Save' }}
49+
</button>
4850
<button @click="showShare = true" class="btn share-btn"
4951
:class="{ 'btn-sharing': isSharing }"
5052
:title="isSharing ? (isGuest ? 'Connected to session' : 'Sharing active') : 'Share session'">
@@ -648,76 +650,73 @@ async function autoJoinShare(sid: string) {
648650
}
649651
650652
// Register callbacks for share lifecycle + relay terminal handling
651-
import { onShareClosed as _onShareClosedSetter, onRelayTerminalCreate as _rtcSetter, onRelayTerminalInput as _rtiSetter, onRelayTerminalResize as _rtrSetter, onRelayTerminalClose as _rtclSetter } from '~/composables/useShare';
653+
import { setOnShareClosed, setOnRelayTerminalCreate, setOnRelayTerminalInput, setOnRelayTerminalResize, setOnRelayTerminalClose } from '~/composables/useShare';
652654
// Map of relay terminalId → local WebSocket (desktop host bridges guest terminals to local PTY)
653655
const relayTerminalWs = new Map<string, WebSocket>();
654656
655657
if (import.meta.client) {
656-
(async () => {
657-
const mod = await import('~/composables/useShare');
658-
mod.onShareClosed = () => {
659-
resetToFolderSelector();
660-
};
658+
setOnShareClosed(() => {
659+
resetToFolderSelector();
660+
});
661661
662-
// Desktop host relay: guest creates a terminal → spawn local terminal, pipe I/O through relay WS
663-
mod.onRelayTerminalCreate = (msg: any) => {
664-
const { terminalId, cwd, cols, rows } = msg;
665-
const { getLocalWsUrl, getSessionId } = useApi();
666-
const { sendRelayMessage } = useShare();
667-
// Connect to the local terminal WS (bypasses share routing to avoid loop)
668-
const localWs = new WebSocket(getLocalWsUrl());
669-
relayTerminalWs.set(terminalId, localWs);
670-
671-
localWs.onopen = () => {
672-
localWs.send(JSON.stringify({
673-
type: "create",
674-
cwd: cwd || rootPath.value,
675-
cols: cols || 80,
676-
rows: rows || 24,
677-
sessionId: getSessionId(),
678-
}));
679-
};
680-
681-
localWs.onmessage = (ev) => {
682-
try {
683-
const data = JSON.parse(ev.data);
684-
if (data.type === "output") {
685-
sendRelayMessage({ type: "terminal-output", terminalId, data: data.data });
686-
} else if (data.type === "exit") {
687-
sendRelayMessage({ type: "terminal-exit", terminalId, code: data.code ?? 0 });
688-
localWs.close();
689-
relayTerminalWs.delete(terminalId);
690-
}
691-
} catch {}
692-
};
693-
694-
localWs.onclose = () => {
695-
relayTerminalWs.delete(terminalId);
696-
};
662+
// Desktop host relay: guest creates a terminal → spawn local terminal, pipe I/O through relay WS
663+
setOnRelayTerminalCreate((msg: any) => {
664+
const { terminalId, cwd, cols, rows } = msg;
665+
const { getLocalWsUrl, getSessionId } = useApi();
666+
const { sendRelayMessage } = useShare();
667+
// Connect to the local terminal WS (bypasses share routing to avoid loop)
668+
const localWs = new WebSocket(getLocalWsUrl());
669+
relayTerminalWs.set(terminalId, localWs);
670+
671+
localWs.onopen = () => {
672+
localWs.send(JSON.stringify({
673+
type: "create",
674+
cwd: cwd || rootPath.value,
675+
cols: cols || 80,
676+
rows: rows || 24,
677+
sessionId: getSessionId(),
678+
}));
697679
};
698680
699-
mod.onRelayTerminalInput = (msg: any) => {
700-
const ws = relayTerminalWs.get(msg.terminalId);
701-
if (ws && ws.readyState === WebSocket.OPEN) {
702-
ws.send(JSON.stringify({ type: "input", data: msg.data }));
703-
}
681+
localWs.onmessage = (ev) => {
682+
try {
683+
const data = JSON.parse(ev.data);
684+
if (data.type === "output") {
685+
sendRelayMessage({ type: "terminal-output", terminalId, data: data.data });
686+
} else if (data.type === "exit") {
687+
sendRelayMessage({ type: "terminal-exit", terminalId, code: data.code ?? 0 });
688+
localWs.close();
689+
relayTerminalWs.delete(terminalId);
690+
}
691+
} catch {}
704692
};
705693
706-
mod.onRelayTerminalResize = (msg: any) => {
707-
const ws = relayTerminalWs.get(msg.terminalId);
708-
if (ws && ws.readyState === WebSocket.OPEN) {
709-
ws.send(JSON.stringify({ type: "resize", cols: msg.cols, rows: msg.rows }));
710-
}
694+
localWs.onclose = () => {
695+
relayTerminalWs.delete(terminalId);
711696
};
697+
});
712698
713-
mod.onRelayTerminalClose = (msg: any) => {
714-
const ws = relayTerminalWs.get(msg.terminalId);
715-
if (ws) {
716-
ws.close();
717-
relayTerminalWs.delete(msg.terminalId);
718-
}
719-
};
720-
})();
699+
setOnRelayTerminalInput((msg: any) => {
700+
const ws = relayTerminalWs.get(msg.terminalId);
701+
if (ws && ws.readyState === WebSocket.OPEN) {
702+
ws.send(JSON.stringify({ type: "input", data: msg.data }));
703+
}
704+
});
705+
706+
setOnRelayTerminalResize((msg: any) => {
707+
const ws = relayTerminalWs.get(msg.terminalId);
708+
if (ws && ws.readyState === WebSocket.OPEN) {
709+
ws.send(JSON.stringify({ type: "resize", cols: msg.cols, rows: msg.rows }));
710+
}
711+
});
712+
713+
setOnRelayTerminalClose((msg: any) => {
714+
const ws = relayTerminalWs.get(msg.terminalId);
715+
if (ws) {
716+
ws.close();
717+
relayTerminalWs.delete(msg.terminalId);
718+
}
719+
});
721720
}
722721
723722
const editorAreaRef = ref<{ splitRatio: number; focusPane: (id: string) => void } | null>(null);

server/routes/_share-terminal.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export default defineWebSocketHandler({
8282
let subs = peerTerminals.get(peer.id);
8383
if (!subs) { subs = new Set(); peerTerminals.set(peer.id, subs); }
8484
subs.add(terminalId);
85+
// Confirm subscription so the client knows it's active and can handle input/resize
86+
const name = session.activeTerminals.get(terminalId) || "";
87+
peer.send(JSON.stringify({ type: "terminal-ready", terminalId, name }));
8588
} else if (data.type === "input") {
8689
const { terminalId, data: inputData } = data;
8790
if (typeof terminalId !== "string" || typeof inputData !== "string") return;

0 commit comments

Comments
 (0)