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
96 changes: 96 additions & 0 deletions frontend/src/renderer/components/TerminalPane.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { WorkspaceSession } from "../types/workspace";
import { TerminalPane } from "./TerminalPane";

vi.mock("./XtermTerminal", () => ({
XtermTerminal: () => <div data-testid="xterm" />,
}));

vi.mock("../hooks/useTerminalSession", () => ({
useTerminalSession: () => ({
attach: vi.fn(),
state: "idle",
error: undefined,
}),
}));

const worker = {
id: "sess-1",
workspaceId: "proj-1",
workspaceName: "my-app",
title: "do the thing",
provider: "claude-code",
kind: "worker",
branch: "ao/sess-1",
status: "working",
updatedAt: "2026-06-10T00:00:00Z",
prs: [],
} satisfies WorkspaceSession;

const orchestrator = {
...worker,
id: "sess-orch",
title: "orchestrate",
kind: "orchestrator",
} satisfies WorkspaceSession;

function renderPane(session?: WorkspaceSession) {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const previousAO = window.ao;
window.ao = {} as typeof window.ao;
const result = render(
<QueryClientProvider client={queryClient}>
<TerminalPane daemonReady fontSize={12} session={session} theme="dark" />
</QueryClientProvider>,
);
return {
...result,
restore: () => {
window.ao = previousAO;
},
};
}

describe("TerminalPane empty states", () => {
it("shows a no-selection message when no session is selected", () => {
const view = renderPane();
try {
expect(screen.getByText("Agent Orchestrator")).toBeInTheDocument();
expect(screen.getByText("No session selected. Pick a worker to attach its terminal.")).toBeInTheDocument();
} finally {
view.restore();
}
});

it("shows a startup message when a selected session has no terminal handle yet", () => {
const view = renderPane(worker);
try {
expect(screen.getByText("Starting session")).toBeInTheDocument();
expect(
screen.getByText(
"Preparing the worker terminal. This can take a moment while AO creates the worktree and starts the agent.",
),
).toBeInTheDocument();
expect(screen.queryByText("No session selected. Pick a worker to attach its terminal.")).not.toBeInTheDocument();
} finally {
view.restore();
}
});

it("shows orchestrator-specific startup copy for a pending orchestrator terminal", () => {
const view = renderPane(orchestrator);
try {
expect(screen.getByText("Starting session")).toBeInTheDocument();
expect(
screen.getByText(
"Preparing the orchestrator terminal. This can take a moment while AO creates the worktree and starts the agent.",
),
).toBeInTheDocument();
expect(screen.queryByText(/worker terminal/i)).not.toBeInTheDocument();
} finally {
view.restore();
}
});
});
12 changes: 8 additions & 4 deletions frontend/src/renderer/components/TerminalPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz
const banner = bannerText(state, error);
const showEmptyState = !handleId;
const showExitedState = state === "exited";
const emptyStateTitle = session ? "Starting session" : "Agent Orchestrator";
const emptyStateMessage = session
? session.kind === "orchestrator"
? "Preparing the orchestrator terminal. This can take a moment while AO creates the worktree and starts the agent."
: "Preparing the worker terminal. This can take a moment while AO creates the worktree and starts the agent."
: "No session selected. Pick a worker to attach its terminal.";

return (
<div className="flex h-full min-h-0 flex-col bg-terminal">
Expand All @@ -231,10 +237,8 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz
{showEmptyState && (
<div className="absolute inset-0 grid place-items-center bg-terminal font-mono text-[13px]">
<div className="text-center">
<div className="text-[var(--term-fg)]">Agent Orchestrator</div>
<div className="mt-2 text-[var(--term-dim)]">
No session selected. Pick a worker to attach its terminal.
</div>
<div className="text-[var(--term-fg)]">{emptyStateTitle}</div>
<div className="mt-2 text-[var(--term-dim)]">{emptyStateMessage}</div>
</div>
</div>
)}
Expand Down
Loading