Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 18 additions & 27 deletions apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ interface RegistryEntry {
class TerminalRuntimeRegistryImpl {
private entries = new Map<string, RegistryEntry>();

private getOrCreate(paneId: string): RegistryEntry {
let entry = this.entries.get(paneId);
private getOrCreate(terminalId: string): RegistryEntry {
let entry = this.entries.get(terminalId);
if (entry) return entry;

entry = {
runtime: createRuntime(paneId),
runtime: createRuntime(terminalId),
transport: createTransport(),
};

this.entries.set(paneId, entry);
this.entries.set(terminalId, entry);
return entry;
}

attach(paneId: string, container: HTMLDivElement, wsUrl: string) {
const { runtime, transport } = this.getOrCreate(paneId);
attach(terminalId: string, container: HTMLDivElement, wsUrl: string) {
const { runtime, transport } = this.getOrCreate(terminalId);

attachToContainer(runtime, container, () => {
sendResize(transport, runtime.terminal.cols, runtime.terminal.rows);
Expand All @@ -46,49 +46,40 @@ class TerminalRuntimeRegistryImpl {
connect(transport, runtime.terminal, wsUrl);
}

/**
* Detach the terminal from its DOM container.
*
* This only removes the DOM attachment (wrapper, resize observer, focus).
* The WebSocket and xterm data flow are intentionally kept alive so output
* written while the pane is hidden is not lost. Disposal of the transport
* happens exclusively through {@link dispose} when the paneId is removed
* from persisted pane state.
*/
detach(paneId: string) {
const entry = this.entries.get(paneId);
detach(terminalId: string) {
const entry = this.entries.get(terminalId);
if (!entry) return;

detachFromContainer(entry.runtime);
}

dispose(paneId: string) {
const entry = this.entries.get(paneId);
dispose(terminalId: string) {
const entry = this.entries.get(terminalId);
if (!entry) return;

sendDispose(entry.transport);
disposeTransport(entry.transport);
disposeRuntime(entry.runtime);

this.entries.delete(paneId);
this.entries.delete(terminalId);
}

getAllPaneIds(): Set<string> {
getAllTerminalIds(): Set<string> {
return new Set(this.entries.keys());
}

has(paneId: string): boolean {
return this.entries.has(paneId);
has(terminalId: string): boolean {
return this.entries.has(terminalId);
}

getConnectionState(paneId: string): ConnectionState {
getConnectionState(terminalId: string): ConnectionState {
return (
this.entries.get(paneId)?.transport.connectionState ?? "disconnected"
this.entries.get(terminalId)?.transport.connectionState ?? "disconnected"
);
}

onStateChange(paneId: string, listener: () => void): () => void {
const { transport } = this.getOrCreate(paneId);
onStateChange(terminalId: string, listener: () => void): () => void {
const { transport } = this.getOrCreate(terminalId);
transport.stateListeners.add(listener);
return () => {
transport.stateListeners.delete(listener);
Expand Down
42 changes: 21 additions & 21 deletions apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const DEFAULT_COLS = 120;
const DEFAULT_ROWS = 32;

export interface TerminalRuntime {
paneId: string;
terminalId: string;
terminal: XTerm;
fitAddon: FitAddon;
serializeAddon: SerializeAddon;
Expand Down Expand Up @@ -49,40 +49,40 @@ function createTerminal(
return { terminal, fitAddon, serializeAddon };
}

function persistBuffer(paneId: string, serializeAddon: SerializeAddon) {
function persistBuffer(terminalId: string, serializeAddon: SerializeAddon) {
try {
const data = serializeAddon.serialize({ scrollback: SERIALIZE_SCROLLBACK });
localStorage.setItem(`${STORAGE_KEY_PREFIX}${paneId}`, data);
localStorage.setItem(`${STORAGE_KEY_PREFIX}${terminalId}`, data);
} catch {}
}

function restoreBuffer(paneId: string, terminal: XTerm) {
function restoreBuffer(terminalId: string, terminal: XTerm) {
try {
const data = localStorage.getItem(`${STORAGE_KEY_PREFIX}${paneId}`);
const data = localStorage.getItem(`${STORAGE_KEY_PREFIX}${terminalId}`);
if (data) terminal.write(data);
} catch {}
}

function clearPersistedBuffer(paneId: string) {
function clearPersistedBuffer(terminalId: string) {
try {
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${paneId}`);
localStorage.removeItem(`${STORAGE_KEY_PREFIX}${terminalId}`);
} catch {}
}

function persistDimensions(paneId: string, cols: number, rows: number) {
function persistDimensions(terminalId: string, cols: number, rows: number) {
try {
localStorage.setItem(
`${DIMS_KEY_PREFIX}${paneId}`,
`${DIMS_KEY_PREFIX}${terminalId}`,
JSON.stringify({ cols, rows }),
);
} catch {}
}

function loadSavedDimensions(
paneId: string,
terminalId: string,
): { cols: number; rows: number } | null {
try {
const raw = localStorage.getItem(`${DIMS_KEY_PREFIX}${paneId}`);
const raw = localStorage.getItem(`${DIMS_KEY_PREFIX}${terminalId}`);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (typeof parsed.cols === "number" && typeof parsed.rows === "number") {
Expand All @@ -94,9 +94,9 @@ function loadSavedDimensions(
}
}

function clearPersistedDimensions(paneId: string) {
function clearPersistedDimensions(terminalId: string) {
try {
localStorage.removeItem(`${DIMS_KEY_PREFIX}${paneId}`);
localStorage.removeItem(`${DIMS_KEY_PREFIX}${terminalId}`);
} catch {}
}

Expand All @@ -112,8 +112,8 @@ function measureAndResize(runtime: TerminalRuntime) {
runtime.lastRows = runtime.terminal.rows;
}

export function createRuntime(paneId: string): TerminalRuntime {
const savedDims = loadSavedDimensions(paneId);
export function createRuntime(terminalId: string): TerminalRuntime {
const savedDims = loadSavedDimensions(terminalId);
const cols = savedDims?.cols ?? DEFAULT_COLS;
const rows = savedDims?.rows ?? DEFAULT_ROWS;

Expand All @@ -123,10 +123,10 @@ export function createRuntime(paneId: string): TerminalRuntime {
wrapper.style.width = "100%";
wrapper.style.height = "100%";
terminal.open(wrapper);
restoreBuffer(paneId, terminal);
restoreBuffer(terminalId, terminal);

return {
paneId,
terminalId,
terminal,
fitAddon,
serializeAddon,
Expand Down Expand Up @@ -163,8 +163,8 @@ export function attachToContainer(
}

export function detachFromContainer(runtime: TerminalRuntime) {
persistBuffer(runtime.paneId, runtime.serializeAddon);
persistDimensions(runtime.paneId, runtime.lastCols, runtime.lastRows);
persistBuffer(runtime.terminalId, runtime.serializeAddon);
persistDimensions(runtime.terminalId, runtime.lastCols, runtime.lastRows);
runtime.resizeObserver?.disconnect();
runtime.resizeObserver = null;
runtime.wrapper.remove();
Expand All @@ -176,6 +176,6 @@ export function disposeRuntime(runtime: TerminalRuntime) {
runtime.resizeObserver = null;
runtime.wrapper.remove();
runtime.terminal.dispose();
clearPersistedBuffer(runtime.paneId);
clearPersistedDimensions(runtime.paneId);
clearPersistedBuffer(runtime.terminalId);
clearPersistedDimensions(runtime.terminalId);
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,53 @@
import type { RendererContext } from "@superset/panes";
import "@xterm/xterm/css/xterm.css";
import { useEffect, useRef, useSyncExternalStore } from "react";
import {
type ConnectionState,
terminalRuntimeRegistry,
} from "renderer/lib/terminal/terminal-runtime-registry";
import { useWorkspaceWsUrl } from "../../../../../providers/WorkspaceTrpcProvider/WorkspaceTrpcProvider";
import type {
PaneViewerData,
TerminalPaneData,
} from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types";
import { useWorkspaceWsUrl } from "renderer/routes/_authenticated/_dashboard/v2-workspace/providers/WorkspaceTrpcProvider/WorkspaceTrpcProvider";

interface TerminalPaneProps {
paneId: string;
ctx: RendererContext<PaneViewerData>;
workspaceId: string;
}

function subscribeToState(paneId: string) {
function subscribeToState(terminalId: string) {
return (callback: () => void) =>
terminalRuntimeRegistry.onStateChange(paneId, callback);
terminalRuntimeRegistry.onStateChange(terminalId, callback);
}

function getConnectionState(paneId: string): ConnectionState {
return terminalRuntimeRegistry.getConnectionState(paneId);
function getConnectionState(terminalId: string): ConnectionState {
return terminalRuntimeRegistry.getConnectionState(terminalId);
}

export function TerminalPane({ paneId, workspaceId }: TerminalPaneProps) {
export function TerminalPane({ ctx, workspaceId }: TerminalPaneProps) {
const { terminalId } = ctx.pane.data as TerminalPaneData;
const containerRef = useRef<HTMLDivElement | null>(null);

const websocketUrl = useWorkspaceWsUrl(`/terminal/${paneId}`, {
const websocketUrl = useWorkspaceWsUrl(`/terminal/${terminalId}`, {
workspaceId,
});

const connectionState = useSyncExternalStore(subscribeToState(paneId), () =>
getConnectionState(paneId),
const connectionState = useSyncExternalStore(
subscribeToState(terminalId),
() => getConnectionState(terminalId),
);

useEffect(() => {
const container = containerRef.current;
if (!container) return;

terminalRuntimeRegistry.attach(paneId, container, websocketUrl);
terminalRuntimeRegistry.attach(terminalId, container, websocketUrl);
Comment thread
Kitenite marked this conversation as resolved.

return () => {
terminalRuntimeRegistry.detach(paneId);
terminalRuntimeRegistry.detach(terminalId);
};
}, [paneId, websocketUrl]);
}, [terminalId, websocketUrl]);

return (
<div className="flex h-full w-full flex-col">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function usePaneRegistry(
getIcon: () => <TerminalSquare className="size-4" />,
getTitle: () => "Terminal",
renderPane: (ctx: RendererContext<PaneViewerData>) => (
<TerminalPane paneId={ctx.pane.id} workspaceId={workspaceId} />
<TerminalPane ctx={ctx} workspaceId={workspaceId} />
),
},
browser: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,16 @@ function V2WorkspacePage() {
<WorkspaceContent
projectId={workspace.projectId}
workspaceId={workspace.id}
workspaceName={workspace.name}
/>
);
}

function WorkspaceContent({
projectId,
workspaceId,
workspaceName,
}: {
projectId: string;
workspaceId: string;
workspaceName: string;
}) {
const { localWorkspaceState, store } = useV2WorkspacePaneLayout({
projectId,
Expand Down Expand Up @@ -146,14 +143,12 @@ function WorkspaceContent({
{
kind: "terminal",
data: {
sessionKey: `${workspaceId}:${crypto.randomUUID()}`,
cwd: `/workspace/${workspaceName}`,
launchMode: "workspace-shell",
terminalId: crypto.randomUUID(),
} as TerminalPaneData,
},
],
});
}, [store, workspaceId, workspaceName]);
}, [store]);

const addChatTab = useCallback(() => {
store.getState().addTab({
Expand Down Expand Up @@ -204,9 +199,7 @@ function WorkspaceContent({
ctx.actions.split(position, {
kind: "terminal",
data: {
sessionKey: `${workspaceId}:${crypto.randomUUID()}`,
cwd: `/workspace/${workspaceName}`,
launchMode: "workspace-shell",
terminalId: crypto.randomUUID(),
} as TerminalPaneData,
});
},
Expand All @@ -220,7 +213,7 @@ function WorkspaceContent({
onClick: (ctx) => ctx.actions.close(),
},
],
[workspaceId, workspaceName],
[],
);

const collections = useCollections();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ export interface FilePaneData {
}

export interface TerminalPaneData {
sessionKey: string;
cwd: string;
launchMode: "workspace-shell" | "command" | "agent";
command?: string;
terminalId: string;
}

export interface ChatPaneData {
Expand Down
Loading
Loading