Skip to content

Commit 3c401f8

Browse files
committed
feat(web): attach terminal channel on connect and runtime bind
- Subscribe to terminal://channel_replay and write to the matched session's terminal, prepending an ANSI clear-screen sequence so replay replaces existing content rather than appending to it. - On WebSocket connect/reconnect (when bootstrapReady), iterate all tabs and sessions and send terminal_channel_attach for each session with a terminalRuntimeId, so the server streams replay followed by live output.
1 parent c7bf155 commit 3c401f8

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

apps/web/src/features/workspace/workspace-sync-hooks.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
subscribeWorkspaceController,
77
subscribeWorkspaceRuntimeState,
88
} from "../../command";
9-
import { subscribeTerminalChannelOutput } from "../../services/terminal-channel/client.ts";
9+
import {
10+
subscribeTerminalChannelOutput,
11+
subscribeTerminalChannelReplay,
12+
sendTerminalChannelAttach,
13+
} from "../../services/terminal-channel/client.ts";
1014
import { type ExecTarget, type Tab, type WorkbenchState, type WorktreeInfo } from "../../state/workbench";
1115
import { getGitChanges } from "../../services/http/git.service";
1216
import { getGitStatus, getWorkspaceTree, getWorktreeList } from "../../services/http/workspace.service";
@@ -296,6 +300,38 @@ export const useWorkspaceTransportSync = ({
296300
return unsubscribe;
297301
}, [schedulePendingStreamFlush, stateRefLatest]);
298302

303+
useEffect(() => {
304+
const unsubscribe = subscribeTerminalChannelReplay(({ runtime_id, data }) => {
305+
const currentState = stateRefLatest.current;
306+
const matchedTab = currentState.tabs.find((tab) => (
307+
tab.sessions.some((session) => session.terminalRuntimeId === runtime_id)
308+
));
309+
if (!matchedTab) {
310+
return;
311+
}
312+
const terminalId = resolveSessionTerminalIdByRuntimeId(
313+
matchedTab.sessions,
314+
runtime_id,
315+
matchedTab.terminals,
316+
);
317+
if (!terminalId) {
318+
return;
319+
}
320+
// Prepend ANSI clear screen + home before replay data so it replaces
321+
// existing terminal content rather than appending to it.
322+
const clearThenData = `\x1b[2J\x1b[H${data}`;
323+
const recorded = recordPendingTerminalStream(pendingStreamIndexRef.current, {
324+
workspaceId: matchedTab.id,
325+
terminalId,
326+
chunk: clearThenData,
327+
});
328+
if (recorded) {
329+
schedulePendingStreamFlush();
330+
}
331+
});
332+
return unsubscribe;
333+
}, [pendingStreamIndexRef, schedulePendingStreamFlush, stateRefLatest]);
334+
299335
useEffect(() => {
300336
const unsubscribe = subscribeWorkspaceController((payload) => {
301337
updateStateRef.current((current) =>
@@ -316,6 +352,15 @@ export const useWorkspaceTransportSync = ({
316352
const unsubscribe = subscribeWsConnectionState(({ kind }) => {
317353
if ((kind !== "connected" && kind !== "reconnected") || !bootstrapReady) return;
318354
void resyncWorkspaceSnapshots(kind === "reconnected");
355+
// Attach terminal channel for each session so the server sends replay + live output.
356+
const currentState = stateRefLatest.current;
357+
for (const tab of currentState.tabs) {
358+
for (const session of tab.sessions) {
359+
if (session.terminalRuntimeId) {
360+
sendTerminalChannelAttach(tab.id, tab.controller.fencingToken, session.terminalRuntimeId);
361+
}
362+
}
363+
}
319364
});
320365
return unsubscribe;
321366
}, [bootstrapReady, resyncWorkspaceSnapshots]);

0 commit comments

Comments
 (0)