From 2162e44432ccf8fcca4607aa6de26db1a223de57 Mon Sep 17 00:00:00 2001 From: moonixt Date: Fri, 29 May 2026 22:53:20 -0300 Subject: [PATCH] fix(terminal): wait for home sidebar geometry --- web-ui/src/App.tsx | 4 +- .../src/hooks/use-home-agent-session.test.tsx | 97 +++++++++++++++++++ web-ui/src/hooks/use-home-agent-session.ts | 56 ++++++++++- .../hooks/use-home-sidebar-agent-panel.tsx | 6 ++ .../terminal/persistent-terminal-manager.ts | 1 + 5 files changed, 160 insertions(+), 4 deletions(-) diff --git a/web-ui/src/App.tsx b/web-ui/src/App.tsx index c9faf0397..02a8151ab 100644 --- a/web-ui/src/App.tsx +++ b/web-ui/src/App.tsx @@ -429,9 +429,12 @@ export default function App(): ReactElement { sendTaskSessionInput, }); const homeTerminalSummary = sessions[homeTerminalTaskId] ?? null; + const sidebarLayout = useProjectNavigationLayout(); const homeSidebarAgentPanel = useHomeSidebarAgentPanel({ currentProjectId, hasNoProjects, + homeSidebarWidth: sidebarLayout.sidebarWidth, + isAgentSectionActive: !selectedCard && homeSidebarSection === "agent" && !sidebarLayout.isCollapsed, runtimeProjectConfig, clineSessionContextVersion, taskSessions: sessions, @@ -687,7 +690,6 @@ export default function App(): ReactElement { return undefined; }, [selectedCard]); - const sidebarLayout = useProjectNavigationLayout(); const handleToggleSidebar = useCallback(() => { sidebarLayout.setSidebarCollapsed(!sidebarLayout.isCollapsed); }, [sidebarLayout]); diff --git a/web-ui/src/hooks/use-home-agent-session.test.tsx b/web-ui/src/hooks/use-home-agent-session.test.tsx index efb861c56..5f32d178b 100644 --- a/web-ui/src/hooks/use-home-agent-session.test.tsx +++ b/web-ui/src/hooks/use-home-agent-session.test.tsx @@ -9,6 +9,9 @@ const startTaskSessionMutateMock = vi.hoisted(() => vi.fn()); const stopTaskSessionMutateMock = vi.hoisted(() => vi.fn()); const reloadTaskChatSessionMutateMock = vi.hoisted(() => vi.fn()); const notifyErrorMock = vi.hoisted(() => vi.fn()); +const clearTerminalGeometryMock = vi.hoisted(() => vi.fn()); +const getTerminalGeometryMock = vi.hoisted(() => vi.fn()); +const prepareWaitForTerminalGeometryMock = vi.hoisted(() => vi.fn()); vi.mock("@/runtime/trpc-client", () => ({ getRuntimeTrpcClient: (workspaceId: string | null) => ({ @@ -30,6 +33,12 @@ vi.mock("@/runtime/task-session-geometry", () => ({ estimateTaskSessionGeometry: () => ({ cols: 120, rows: 24 }), })); +vi.mock("@/terminal/terminal-geometry-registry", () => ({ + clearTerminalGeometry: clearTerminalGeometryMock, + getTerminalGeometry: getTerminalGeometryMock, + prepareWaitForTerminalGeometry: prepareWaitForTerminalGeometryMock, +})); + vi.mock("@/components/app-toaster", () => ({ notifyError: notifyErrorMock, })); @@ -162,16 +171,20 @@ function requireTaskId(taskId: string | null): string { } function HookHarness({ + canStartTerminalSession = true, config, clineSessionContextVersion = 0, currentProjectId, + homeSidebarWidth = 432, onSnapshot, workspaceGit = DEFAULT_WORKSPACE_GIT, seedSessionSummary = false, }: { + canStartTerminalSession?: boolean; config: RuntimeConfigResponse | null; clineSessionContextVersion?: number; currentProjectId: string | null; + homeSidebarWidth?: number; onSnapshot: (snapshot: HookSnapshot) => void; workspaceGit?: RuntimeGitRepositoryInfo | null; seedSessionSummary?: boolean; @@ -184,7 +197,9 @@ function HookHarness({ })); }, []); const result = useHomeAgentSession({ + canStartTerminalSession, currentProjectId, + homeSidebarWidth, runtimeProjectConfig: config, workspaceGit, clineSessionContextVersion, @@ -229,6 +244,11 @@ describe("useHomeAgentSession", () => { summary: createSummary(taskId, "cline"), })); notifyErrorMock.mockReset(); + clearTerminalGeometryMock.mockReset(); + getTerminalGeometryMock.mockReset(); + getTerminalGeometryMock.mockReturnValue(null); + prepareWaitForTerminalGeometryMock.mockReset(); + prepareWaitForTerminalGeometryMock.mockReturnValue(async () => undefined); previousActEnvironment = (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }) .IS_REACT_ACT_ENVIRONMENT; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -308,6 +328,83 @@ describe("useHomeAgentSession", () => { expect(rotatedSnapshot.sessionKeys).toEqual([rotatedSnapshot.taskId]); }); + it("starts home terminal sessions with reported terminal geometry when available", async () => { + getTerminalGeometryMock.mockReturnValue({ cols: 72, rows: 31 }); + let latestSnapshot: HookSnapshot | null = null; + + await act(async () => { + root.render( + { + latestSnapshot = snapshot; + }} + />, + ); + await createFlushPromises(); + }); + + const snapshot = requireSnapshot(latestSnapshot); + expect(snapshot.taskId).toBe("__home_agent__:workspace-1:codex"); + expect(clearTerminalGeometryMock).toHaveBeenCalledWith(snapshot.taskId); + expect(prepareWaitForTerminalGeometryMock).toHaveBeenCalledWith(snapshot.taskId); + expect(startTaskSessionMutateMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + taskId: snapshot.taskId, + cols: 72, + rows: 31, + }), + ); + }); + + it("waits for the home agent panel to be visible before starting terminal sessions", async () => { + let latestSnapshot: HookSnapshot | null = null; + + await act(async () => { + root.render( + { + latestSnapshot = snapshot; + }} + />, + ); + await createFlushPromises(); + }); + + const hiddenSnapshot = requireSnapshot(latestSnapshot); + expect(hiddenSnapshot.panelMode).toBe("terminal"); + expect(hiddenSnapshot.taskId).toBe("__home_agent__:workspace-1:codex"); + expect(startTaskSessionMutateMock).not.toHaveBeenCalled(); + + getTerminalGeometryMock.mockReturnValue({ cols: 58, rows: 28 }); + await act(async () => { + root.render( + { + latestSnapshot = snapshot; + }} + />, + ); + await createFlushPromises(); + }); + + expect(startTaskSessionMutateMock).toHaveBeenCalledTimes(1); + expect(startTaskSessionMutateMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + taskId: hiddenSnapshot.taskId, + cols: 58, + rows: 28, + }), + ); + }); + it("does not restart the home terminal session on a no-op rerender", async () => { let latestSnapshot: HookSnapshot | null = null; diff --git a/web-ui/src/hooks/use-home-agent-session.ts b/web-ui/src/hooks/use-home-agent-session.ts index 44d88bd77..602441f49 100644 --- a/web-ui/src/hooks/use-home-agent-session.ts +++ b/web-ui/src/hooks/use-home-agent-session.ts @@ -12,6 +12,14 @@ import { getRuntimeClineProviderSettings, isNativeClineAgentSelected } from "@/r import { estimateTaskSessionGeometry } from "@/runtime/task-session-geometry"; import { getRuntimeTrpcClient } from "@/runtime/trpc-client"; import type { RuntimeConfigResponse, RuntimeGitRepositoryInfo, RuntimeTaskSessionSummary } from "@/runtime/types"; +import { + clearTerminalGeometry, + getTerminalGeometry, + prepareWaitForTerminalGeometry, +} from "@/terminal/terminal-geometry-registry"; + +const APPROX_TERMINAL_CELL_WIDTH_PX = 8; +const HOME_AGENT_TERMINAL_HORIZONTAL_CHROME_PX = 24; type HomeAgentPanelMode = "chat" | "terminal"; @@ -22,7 +30,9 @@ interface HomeAgentDescriptor { } interface UseHomeAgentSessionInput { + canStartTerminalSession: boolean; currentProjectId: string | null; + homeSidebarWidth: number; runtimeProjectConfig: RuntimeConfigResponse | null; workspaceGit: RuntimeGitRepositoryInfo | null; clineSessionContextVersion: number; @@ -94,6 +104,18 @@ function buildHomeAgentSessionKey(session: HomeAgentSessionIdentity): string { return `${session.workspaceId}:${session.taskId}`; } +function estimateHomeAgentSessionGeometry(homeSidebarWidth: number): { cols: number; rows: number } { + const fallbackGeometry = estimateTaskSessionGeometry(window.innerWidth, window.innerHeight); + if (!Number.isFinite(homeSidebarWidth) || homeSidebarWidth <= 0) { + return fallbackGeometry; + } + const terminalWidth = Math.max(0, homeSidebarWidth - HOME_AGENT_TERMINAL_HORIZONTAL_CHROME_PX); + return { + cols: Math.max(20, Math.floor(terminalWidth / APPROX_TERMINAL_CELL_WIDTH_PX)), + rows: fallbackGeometry.rows, + }; +} + async function stopHomeAgentSession(session: HomeAgentSessionIdentity | null): Promise { if (!session) { return; @@ -108,7 +130,9 @@ async function stopHomeAgentSession(session: HomeAgentSessionIdentity | null): P } export function useHomeAgentSession({ + canStartTerminalSession, currentProjectId, + homeSidebarWidth, runtimeProjectConfig, workspaceGit, clineSessionContextVersion, @@ -124,7 +148,11 @@ export function useHomeAgentSession({ const previousClineSessionContextVersionByWorkspaceRef = useRef(new Map()); const nextStartRequestIdRef = useRef(0); const disposedRef = useRef(false); + const canStartTerminalSessionRef = useRef(canStartTerminalSession); + const homeSidebarWidthRef = useRef(homeSidebarWidth); const clineProviderSettings = getRuntimeClineProviderSettings(runtimeProjectConfig); + canStartTerminalSessionRef.current = canStartTerminalSession; + homeSidebarWidthRef.current = homeSidebarWidth; useEffect(() => { latestBaseRefRef.current = resolveHomeAgentBaseRef(workspaceGit); @@ -284,7 +312,7 @@ export function useHomeAgentSession({ }, [clineSessionContextVersion, currentProjectId, descriptor, sessionSummaries, upsertSessionSummary]); useEffect(() => { - if (!currentProjectId || !descriptor || descriptor.panelMode !== "terminal") { + if (!currentProjectId || !descriptor || descriptor.panelMode !== "terminal" || !canStartTerminalSession) { return; } @@ -312,7 +340,24 @@ export function useHomeAgentSession({ void (async () => { try { - const geometry = estimateTaskSessionGeometry(window.innerWidth, window.innerHeight); + clearTerminalGeometry(session.taskId); + const waitForTerminalGeometry = prepareWaitForTerminalGeometry(session.taskId); + await waitForTerminalGeometry(); + if (!canStartTerminalSessionRef.current) { + if (pendingStartRequestIdsRef.current.get(sessionKey) === requestId) { + pendingStartRequestIdsRef.current.delete(sessionKey); + } + return; + } + if ( + disposedRef.current || + pendingStartRequestIdsRef.current.get(sessionKey) !== requestId || + desiredTaskIdByWorkspaceRef.current.get(session.workspaceId) !== session.taskId + ) { + return; + } + const geometry = + getTerminalGeometry(session.taskId) ?? estimateHomeAgentSessionGeometry(homeSidebarWidthRef.current); const trpcClient = getRuntimeTrpcClient(session.workspaceId); const response = await trpcClient.runtime.startTaskSession.mutate({ taskId: session.taskId, @@ -331,6 +376,11 @@ export function useHomeAgentSession({ } pendingStartRequestIdsRef.current.delete(sessionKey); + if (!canStartTerminalSessionRef.current) { + await stopHomeAgentSession(session); + return; + } + if (desiredTaskIdByWorkspaceRef.current.get(session.workspaceId) !== session.taskId) { await stopHomeAgentSession(session); return; @@ -357,7 +407,7 @@ export function useHomeAgentSession({ notifyError(message); } })(); - }, [currentProjectId, descriptor, sessionSummaries, upsertSessionSummary]); + }, [canStartTerminalSession, currentProjectId, descriptor, upsertSessionSummary]); useEffect(() => { return () => { diff --git a/web-ui/src/hooks/use-home-sidebar-agent-panel.tsx b/web-ui/src/hooks/use-home-sidebar-agent-panel.tsx index 477bfb7c3..7737267ef 100644 --- a/web-ui/src/hooks/use-home-sidebar-agent-panel.tsx +++ b/web-ui/src/hooks/use-home-sidebar-agent-panel.tsx @@ -26,6 +26,8 @@ import { useTerminalThemeColors } from "@/terminal/theme-colors"; interface UseHomeSidebarAgentPanelInput { currentProjectId: string | null; hasNoProjects: boolean; + homeSidebarWidth: number; + isAgentSectionActive: boolean; runtimeProjectConfig: RuntimeConfigResponse | null; clineSessionContextVersion: number; taskSessions: Record; @@ -47,6 +49,8 @@ async function stopHomeSidebarTaskSession(workspaceId: string, taskId: string): export function useHomeSidebarAgentPanel({ currentProjectId, hasNoProjects, + homeSidebarWidth, + isAgentSectionActive, runtimeProjectConfig, clineSessionContextVersion, taskSessions, @@ -81,7 +85,9 @@ export function useHomeSidebarAgentPanel({ return mergedSessionSummaries; }, [sessionSummaries, taskSessions]); const { panelMode, taskId } = useHomeAgentSession({ + canStartTerminalSession: isAgentSectionActive, currentProjectId, + homeSidebarWidth, runtimeProjectConfig, workspaceGit, clineSessionContextVersion, diff --git a/web-ui/src/terminal/persistent-terminal-manager.ts b/web-ui/src/terminal/persistent-terminal-manager.ts index e74b277f6..a421e1fc5 100644 --- a/web-ui/src/terminal/persistent-terminal-manager.ts +++ b/web-ui/src/terminal/persistent-terminal-manager.ts @@ -615,6 +615,7 @@ class PersistentTerminal { return; } this.terminal.reset(); + this.requestResize(); }); }