diff --git a/apps/desktop/src/lib/trpc/routers/terminal/terminal.ts b/apps/desktop/src/lib/trpc/routers/terminal/terminal.ts index 7ddaa8a2399..6a4875f1f9c 100644 --- a/apps/desktop/src/lib/trpc/routers/terminal/terminal.ts +++ b/apps/desktop/src/lib/trpc/routers/terminal/terminal.ts @@ -96,6 +96,7 @@ export const createTerminalRouter = () => { isNew: result.isNew, scrollback: result.scrollback, wasRecovered: result.wasRecovered, + viewportY: result.viewportY, }; }), @@ -151,6 +152,7 @@ export const createTerminalRouter = () => { .input( z.object({ paneId: z.string(), + viewportY: z.number().optional(), }), ) .mutation(async ({ input }) => { diff --git a/apps/desktop/src/main/lib/terminal/manager.ts b/apps/desktop/src/main/lib/terminal/manager.ts index 5603ec2fcbc..09c5e367beb 100644 --- a/apps/desktop/src/main/lib/terminal/manager.ts +++ b/apps/desktop/src/main/lib/terminal/manager.ts @@ -41,6 +41,7 @@ export class TerminalManager extends EventEmitter { isNew: false, scrollback: getSerializedScrollback(existing), wasRecovered: existing.wasRecovered, + viewportY: existing.viewportY, }; } @@ -238,8 +239,8 @@ export class TerminalManager extends EventEmitter { } } - detach(params: { paneId: string }): void { - const { paneId } = params; + detach(params: { paneId: string; viewportY?: number }): void { + const { paneId, viewportY } = params; const session = this.sessions.get(paneId); if (!session) { @@ -248,6 +249,9 @@ export class TerminalManager extends EventEmitter { } session.lastActive = Date.now(); + if (viewportY !== undefined) { + session.viewportY = viewportY; + } } clearScrollback(params: { paneId: string }): void { diff --git a/apps/desktop/src/main/lib/terminal/types.ts b/apps/desktop/src/main/lib/terminal/types.ts index 1fdcf4169ca..f5968d218b9 100644 --- a/apps/desktop/src/main/lib/terminal/types.ts +++ b/apps/desktop/src/main/lib/terminal/types.ts @@ -19,6 +19,8 @@ export interface TerminalSession { shell: string; startTime: number; usedFallback: boolean; + /** Saved viewport scroll position for restoration on reattach */ + viewportY?: number; } export interface TerminalDataEvent { @@ -38,6 +40,8 @@ export interface SessionResult { isNew: boolean; scrollback: string; wasRecovered: boolean; + /** Saved viewport scroll position for restoration on reattach */ + viewportY?: number; } export interface CreateSessionParams { diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index 03248c8f68c..5d1d21b5a2a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -380,9 +380,15 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { wasRecovered: boolean; isNew: boolean; scrollback: string; + viewportY?: number; }) => { - xterm.write(result.scrollback); - updateCwdRef.current(result.scrollback); + // Callback ensures scroll restoration happens after content is rendered + xterm.write(result.scrollback, () => { + updateCwdRef.current(result.scrollback); + if (result.viewportY !== undefined) { + xterm.scrollToLine(result.viewportY); + } + }); }; const restartTerminal = () => { @@ -561,8 +567,9 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { unregisterClearCallbackRef.current(paneId); unregisterScrollToBottomCallbackRef.current(paneId); debouncedSetTabAutoTitleRef.current?.cancel?.(); + const viewportY = xterm.buffer.active.viewportY; // Detach instead of kill to keep PTY running for reattachment - detachRef.current({ paneId }); + detachRef.current({ paneId, viewportY }); setSubscriptionEnabled(false); xterm.dispose(); xtermRef.current = null;