Skip to content
Open
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
4 changes: 3 additions & 1 deletion web-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -687,7 +690,6 @@ export default function App(): ReactElement {
return undefined;
}, [selectedCard]);

const sidebarLayout = useProjectNavigationLayout();
const handleToggleSidebar = useCallback(() => {
sidebarLayout.setSidebarCollapsed(!sidebarLayout.isCollapsed);
}, [sidebarLayout]);
Expand Down
97 changes: 97 additions & 0 deletions web-ui/src/hooks/use-home-agent-session.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -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,
}));
Expand Down Expand Up @@ -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;
Expand All @@ -184,7 +197,9 @@ function HookHarness({
}));
}, []);
const result = useHomeAgentSession({
canStartTerminalSession,
currentProjectId,
homeSidebarWidth,
runtimeProjectConfig: config,
workspaceGit,
clineSessionContextVersion,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
<HookHarness
config={createRuntimeConfig()}
currentProjectId="workspace-1"
onSnapshot={(snapshot) => {
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(
<HookHarness
canStartTerminalSession={false}
config={createRuntimeConfig()}
currentProjectId="workspace-1"
onSnapshot={(snapshot) => {
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(
<HookHarness
canStartTerminalSession
config={createRuntimeConfig()}
currentProjectId="workspace-1"
onSnapshot={(snapshot) => {
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;

Expand Down
56 changes: 53 additions & 3 deletions web-ui/src/hooks/use-home-agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -22,7 +30,9 @@ interface HomeAgentDescriptor {
}

interface UseHomeAgentSessionInput {
canStartTerminalSession: boolean;
currentProjectId: string | null;
homeSidebarWidth: number;
runtimeProjectConfig: RuntimeConfigResponse | null;
workspaceGit: RuntimeGitRepositoryInfo | null;
clineSessionContextVersion: number;
Expand Down Expand Up @@ -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<void> {
if (!session) {
return;
Expand All @@ -108,7 +130,9 @@ async function stopHomeAgentSession(session: HomeAgentSessionIdentity | null): P
}

export function useHomeAgentSession({
canStartTerminalSession,
currentProjectId,
homeSidebarWidth,
runtimeProjectConfig,
workspaceGit,
clineSessionContextVersion,
Expand All @@ -124,7 +148,11 @@ export function useHomeAgentSession({
const previousClineSessionContextVersionByWorkspaceRef = useRef(new Map<string, number>());
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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -357,7 +407,7 @@ export function useHomeAgentSession({
notifyError(message);
}
})();
}, [currentProjectId, descriptor, sessionSummaries, upsertSessionSummary]);
}, [canStartTerminalSession, currentProjectId, descriptor, upsertSessionSummary]);

useEffect(() => {
return () => {
Expand Down
6 changes: 6 additions & 0 deletions web-ui/src/hooks/use-home-sidebar-agent-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, RuntimeTaskSessionSummary>;
Expand All @@ -47,6 +49,8 @@ async function stopHomeSidebarTaskSession(workspaceId: string, taskId: string):
export function useHomeSidebarAgentPanel({
currentProjectId,
hasNoProjects,
homeSidebarWidth,
isAgentSectionActive,
runtimeProjectConfig,
clineSessionContextVersion,
taskSessions,
Expand Down Expand Up @@ -81,7 +85,9 @@ export function useHomeSidebarAgentPanel({
return mergedSessionSummaries;
}, [sessionSummaries, taskSessions]);
const { panelMode, taskId } = useHomeAgentSession({
canStartTerminalSession: isAgentSectionActive,
currentProjectId,
homeSidebarWidth,
runtimeProjectConfig,
workspaceGit,
clineSessionContextVersion,
Expand Down
1 change: 1 addition & 0 deletions web-ui/src/terminal/persistent-terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ class PersistentTerminal {
return;
}
this.terminal.reset();
this.requestResize();
});
}

Expand Down