refactor: remove terminal agent concept entirely

Remove the "agent can be a terminal" branching from the entire codebase.
An agent is now always a session-backed chat agent. Standalone terminal
infrastructure (terminal component, ANSI handling, terminal-stream-protocol)
is preserved.

Server: delete ManagedTerminalAgent, AgentKind, TerminalExitDetails,
launchTerminalAgent, registerTerminalAgent, handleTerminalAgentExited,
supportsTerminalMode capability, buildTerminalCreate/ResumeCommand from
all providers, terminal agent persistence/projections.

App: delete terminal-agent-panel.tsx, terminal-agent-reopen-store.ts,
terminal/terminalExit fields on agent state, "Terminal Agents" launcher
section, terminal-agent workspace setup flow, terminal badge in agent list.

CLI: remove terminal column from ls, terminal-agent error from send.

60 files changed, -3592 lines
This commit is contained in:
Mohamed Boudra
2026-04-02 10:55:54 +07:00
parent 59cb4d7499
commit 060f3d457c
60 changed files with 60 additions and 3592 deletions
-12
View File
@@ -89,12 +89,6 @@ export async function waitForLauncherPanel(page: Page): Promise<void> {
});
}
/** Assert that the launcher panel shows provider tiles under "Terminal Agents". */
export async function assertProviderTilesVisible(page: Page): Promise<void> {
await expect(page.getByText("Terminal Agents", { exact: true }).first()).toBeVisible({
timeout: 10_000,
});
}
/** Assert the launcher panel has a "New Chat" tile. */
export async function assertNewChatTileVisible(page: Page): Promise<void> {
@@ -122,12 +116,6 @@ export async function clickTerminal(page: Page): Promise<void> {
await button.click();
}
/** Click a provider tile by label (e.g. "Claude Code", "Codex"). */
export async function clickProviderTile(page: Page, providerLabel: string): Promise<void> {
const tile = page.getByRole("button", { name: providerLabel }).first();
await expect(tile).toBeVisible({ timeout: 10_000 });
await tile.click();
}
// ─── Tab title assertions ──────────────────────────────────────────────────
@@ -1,11 +1,9 @@
import { expect, type Page } from "@playwright/test";
import {
clickNewChat,
clickProviderTile,
clickTerminal,
countTabsOfKind,
getTabTestIds,
waitForTabWithTitle,
} from "./launcher";
import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf";
@@ -35,13 +33,6 @@ export async function createStandaloneTerminalFromLauncher(page: Page): Promise<
await expect.poll(async () => (await getTabTestIds(page)).length).toBe(tabIdsBefore.length);
}
export async function createTerminalAgentFromLauncher(page: Page, providerLabel: string): Promise<void> {
await clickProviderTile(page, providerLabel);
await expect(page.getByTestId("terminal-agent-loading")).toHaveCount(0, { timeout: 30_000 });
await expect(terminalSurface(page)).toBeVisible({ timeout: 30_000 });
await waitForTabWithTitle(page, /new agent/i);
}
export async function createAgentChatFromLauncher(page: Page): Promise<void> {
await clickNewChat(page);
await expect(composerInput(page)).toBeVisible({ timeout: 15_000 });
@@ -186,26 +186,6 @@ export async function createChatAgentFromWorkspaceSetup(
await dialog.getByRole("button", { name: "Send message" }).click();
}
export async function createTerminalAgentFromWorkspaceSetup(
page: Page,
input: { providerLabel: string; prompt?: string },
): Promise<void> {
const dialog = workspaceSetupDialog(page);
await dialog.getByRole("button", { name: /Terminal Agent/i }).click();
const providerButton = dialog.getByRole("button", { name: new RegExp(`^${input.providerLabel}$`, "i") }).first();
await expect(providerButton).toBeVisible({ timeout: 15_000 });
await providerButton.click();
if (input.prompt) {
const promptInput = dialog.getByPlaceholder("Optional").first();
await expect(promptInput).toBeVisible({ timeout: 15_000 });
await promptInput.fill(input.prompt);
}
await dialog.getByRole("button", { name: "Launch" }).click();
}
export async function createStandaloneTerminalFromWorkspaceSetup(page: Page): Promise<void> {
await workspaceSetupDialog(page)
.getByRole("button", { name: /^Terminal Create the workspace/i })
+1 -59
View File
@@ -3,7 +3,6 @@ import { createTempGitRepo } from "./helpers/workspace";
import {
gotoWorkspace,
waitForLauncherPanel,
assertProviderTilesVisible,
assertNewChatTileVisible,
assertTerminalTileVisible,
assertSingleNewTabButton,
@@ -11,7 +10,6 @@ import {
pressNewTabShortcut,
clickNewChat,
clickTerminal,
clickProviderTile,
countTabsOfKind,
getTabTestIds,
waitForTabWithTitle,
@@ -49,7 +47,7 @@ test.afterAll(async () => {
// ═══════════════════════════════════════════════════════════════════════════
test.describe("Launcher tab", () => {
test("Cmd+T opens launcher panel with New Chat, Terminal, and provider tiles", async ({
test("Cmd+T opens launcher panel with New Chat and Terminal tiles", async ({
page,
}) => {
await gotoWorkspace(page, workspaceId);
@@ -59,7 +57,6 @@ test.describe("Launcher tab", () => {
await waitForLauncherPanel(page);
await assertNewChatTileVisible(page);
await assertTerminalTileVisible(page);
await assertProviderTilesVisible(page);
});
test("opening two new tabs creates two launcher tabs", async ({ page }) => {
@@ -126,61 +123,6 @@ test.describe("Launcher tab", () => {
expect(terminalTabs.length).toBeGreaterThanOrEqual(1);
});
test("clicking a provider tile replaces launcher with terminal agent tab", async ({ page }) => {
test.setTimeout(45_000);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
const tabsBefore = await getTabTestIds(page);
// Click the first visible provider tile under "Terminal Agents"
const providerTiles = page.locator('[role="button"]').filter({
has: page.locator("text=Terminal Agents").locator("..").locator(".."),
});
// Try clicking any provider tile — find the first one after the "Terminal Agents" label
const terminalAgentsLabel = page.getByText("Terminal Agents", { exact: true }).first();
await expect(terminalAgentsLabel).toBeVisible({ timeout: 10_000 });
// The provider grid follows the label. Click the first provider tile.
const providerGrid = terminalAgentsLabel.locator("~ *").first();
const firstProvider = providerGrid.getByRole("button").first();
if (await firstProvider.isVisible().catch(() => false)) {
await firstProvider.click();
} else {
// Fallback: look for any provider button after the section label
const allButtons = page.getByRole("button");
const count = await allButtons.count();
let clicked = false;
for (let i = 0; i < count; i++) {
const btn = allButtons.nth(i);
const text = await btn.innerText().catch(() => "");
// Skip known non-provider buttons
if (["New Chat", "Terminal", "More", "+"].includes(text.trim())) continue;
if (!text.trim()) continue;
await btn.click();
clicked = true;
break;
}
if (!clicked) {
test.skip(true, "No provider tiles available");
return;
}
}
// Should see an agent panel (terminal surface or agent stream)
const agentOrTerminal = page.locator(
'[data-testid="terminal-surface"], [data-testid^="agent-"]',
);
await expect(agentOrTerminal.first()).toBeVisible({ timeout: 30_000 });
// Tab count stays the same (replaced, not added)
const tabsAfter = await getTabTestIds(page);
expect(tabsAfter.length).toBe(tabsBefore.length);
});
test("tab bar shows a single + button per pane", async ({ page }) => {
await gotoWorkspace(page, workspaceId);
await assertSingleNewTabButton(page);
@@ -10,7 +10,6 @@ import { createTempGitRepo } from "./helpers/workspace";
import {
createAgentChatFromLauncher,
createStandaloneTerminalFromLauncher,
createTerminalAgentFromLauncher,
expectTerminalCwd,
} from "./helpers/workspace-lifecycle";
import { connectWorkspaceSetupClient, seedProjectForWorkspaceSetup } from "./helpers/workspace-setup";
@@ -22,30 +21,6 @@ test.describe("Workspace lifecycle", () => {
test.describe.configure({ retries: 1 });
test.describe("Main checkout", () => {
test("creates a terminal agent via provider tile", async ({ page }) => {
test.setTimeout(60_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-main-agent-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspaceResult = await client.openProject(repo.path);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${repo.path}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createTerminalAgentFromLauncher(page, "Claude");
} finally {
await client.close();
await repo.cleanup();
}
});
test("creates an agent chat via New Chat", async ({ page }) => {
test.setTimeout(60_000);
@@ -97,53 +72,6 @@ test.describe("Workspace lifecycle", () => {
});
test.describe("Worktree workspace", () => {
test("creates a terminal agent via provider tile", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("lifecycle-wt-agent-");
const worktreePath = path.join(
"/tmp",
`paseo-wt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
const branchName = `lifecycle-wt-agent-${Date.now()}`;
let worktreeCreated = false;
try {
await seedProjectForWorkspaceSetup(client, repo.path);
execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branchName)} main`, {
cwd: repo.path,
stdio: "ignore",
});
worktreeCreated = true;
const workspaceResult = await client.openProject(worktreePath);
if (!workspaceResult.workspace) {
throw new Error(workspaceResult.error ?? `Failed to open project ${worktreePath}`);
}
const workspaceId = String(workspaceResult.workspace.id);
await gotoWorkspace(page, workspaceId);
await clickNewTabButton(page);
await waitForLauncherPanel(page);
await createTerminalAgentFromLauncher(page, "Claude");
} finally {
if (worktreeCreated) {
try {
execSync(`git worktree remove ${JSON.stringify(worktreePath)} --force`, {
cwd: repo.path,
stdio: "ignore",
});
} catch {
// Best-effort cleanup so test failures preserve the original error.
}
}
await client.close();
await repo.cleanup();
}
});
test("creates an agent chat via New Chat", async ({ page }) => {
test.setTimeout(90_000);
@@ -5,7 +5,6 @@ import {
connectWorkspaceSetupClient,
createChatAgentFromWorkspaceSetup,
createStandaloneTerminalFromWorkspaceSetup,
createTerminalAgentFromWorkspaceSetup,
createWorkspaceFromSidebar,
findWorktreeWorkspaceForProject,
openHomeWithProject,
@@ -109,36 +108,6 @@ test.describe("Workspace setup runtime authority", () => {
}
});
test("first terminal agent attaches to the created workspace", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("workspace-setup-terminal-agent-");
try {
await client.openProject(repo.path);
await openWorkspaceSetupDialogFromSidebar(page, repo.path);
const agentIdsBefore = new Set((await client.fetchAgents()).entries.map((entry) => entry.agent.id));
await createTerminalAgentFromWorkspaceSetup(page, {
providerLabel: "Claude",
prompt: `workspace-setup-terminal-agent-${Date.now()}`,
});
const workspace = await expectCreatedWorkspaceRoute(client, repo.path);
const agent = await waitForNewWorkspaceAgent(
client,
workspace.workspaceDirectory,
agentIdsBefore,
);
expect(agent.cwd).toBe(workspace.workspaceDirectory);
expect(agent.cwd).not.toBe(repo.path);
} finally {
await client.close();
await repo.cleanup();
}
});
test("first terminal attaches to the created workspace", async ({ page }) => {
test.setTimeout(90_000);