* feat(app): add persistent workspace side panes Keep the common review and file-browsing workflow beside the active agent without tearing down pane contents. Workspace tabs now share one compact chip language, assistant file links route into the retained right pane, and diff/file trees share stable geometry. * fix(app): localize pull request empty state * test(app): update explorer pane coverage * fix(app): preserve explorer pane workflows * test(app): make explorer tab selection idempotent * fix(app): persist workspace review pins * fix(app): preserve sidebar scrim drag hook * fix(app): restore companion pane interactions * test(app): follow selected sidebar surface
157 lines
5.9 KiB
TypeScript
157 lines
5.9 KiB
TypeScript
import { test, expect } from "../support/fixtures";
|
|
import { expectComposerVisible, submitMessage } from "../support/helpers/composer";
|
|
import { delayCreatedAgentInitialTailResponse } from "../support/helpers/agent-timeline-gate";
|
|
import { delayBrowserAgentCreatedStatus } from "../support/helpers/new-workspace";
|
|
import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client";
|
|
import {
|
|
createAgentTabFromMenu,
|
|
waitForWorkspaceTabsVisible,
|
|
} from "../support/helpers/workspace-tabs";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
|
|
|
async function waitForCreatedAgentId(
|
|
client: SeedDaemonClient,
|
|
input: { cwd: string; workspaceId: string },
|
|
): Promise<string> {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const result = await client.fetchAgents({ scope: "active" });
|
|
return result.entries
|
|
.filter(
|
|
(entry) =>
|
|
entry.agent.cwd === input.cwd && entry.agent.workspaceId === input.workspaceId,
|
|
)
|
|
.map((entry) => entry.agent.id);
|
|
},
|
|
{ timeout: 30_000 },
|
|
)
|
|
.toHaveLength(1);
|
|
const result = await client.fetchAgents({ scope: "active" });
|
|
const agent = result.entries.find(
|
|
(entry) => entry.agent.cwd === input.cwd && entry.agent.workspaceId === input.workspaceId,
|
|
);
|
|
if (!agent) {
|
|
throw new Error(`Expected one created agent in ${input.cwd}`);
|
|
}
|
|
return agent.agent.id;
|
|
}
|
|
|
|
async function fetchActiveAgentTitle(
|
|
client: SeedDaemonClient,
|
|
agentId: string,
|
|
): Promise<string | null> {
|
|
const result = await client.fetchAgents({ scope: "active" });
|
|
return result.entries.find((entry) => entry.agent.id === agentId)?.agent.title ?? null;
|
|
}
|
|
|
|
test.describe("Workspace agent title handoff", () => {
|
|
test("does not cover the agent pane while the optimistic create becomes authoritative", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
|
const timelineGate = await delayCreatedAgentInitialTailResponse(page);
|
|
const workspace = await seedWorkspace({ repoPrefix: "workspace-create-handoff-flash-" });
|
|
|
|
try {
|
|
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await createAgentTabFromMenu(page);
|
|
await expectComposerVisible(page);
|
|
|
|
const prompt = "Keep the optimistic agent pane visible during handoff";
|
|
await submitMessage(page, prompt);
|
|
const agentId = await timelineGate.waitForCreatedAgent();
|
|
await timelineGate.waitForDelayedResponse();
|
|
|
|
await expect(page.getByTestId(`workspace-tab-agent_${agentId}`).first()).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
await expect(page.getByText(prompt, { exact: true }).first()).toBeVisible();
|
|
await expect(page.getByTestId("agent-history-overlay")).toHaveCount(0);
|
|
|
|
const overlayAppeared = page
|
|
.getByTestId("agent-history-overlay")
|
|
.waitFor({ state: "attached", timeout: 2_000 })
|
|
.then(
|
|
() => true,
|
|
() => false,
|
|
);
|
|
timelineGate.release();
|
|
await timelineGate.waitForForwardedResponse();
|
|
|
|
expect(await overlayAppeared).toBe(false);
|
|
} finally {
|
|
timelineGate.release();
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("shows the prompt tab title and replaces it when the daemon title updates", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
|
|
const agentCreatedDelay = await delayBrowserAgentCreatedStatus(page);
|
|
const workspace = await seedWorkspace({ repoPrefix: "workspace-title-handoff-" });
|
|
|
|
try {
|
|
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
|
|
await waitForWorkspaceTabsVisible(page);
|
|
await createAgentTabFromMenu(page);
|
|
await expectComposerVisible(page);
|
|
|
|
const promptTitle = "Investigate optimistic tab title handoff";
|
|
const generatedTitle = "Generated Handoff Title";
|
|
await submitMessage(page, `${promptTitle}\n\nMake the UI state deterministic.`);
|
|
await agentCreatedDelay.waitForCreateRequest();
|
|
await agentCreatedDelay.waitForDelayedCreatedStatus();
|
|
|
|
await expect(page.getByRole("button", { name: promptTitle }).first()).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
await expect(
|
|
page.getByText(/Loading agent title|Loading\.\.\./).filter({ visible: true }),
|
|
).toHaveCount(0);
|
|
|
|
const agentId = await waitForCreatedAgentId(workspace.client, {
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
});
|
|
|
|
await expect(page.getByTestId(`workspace-tab-agent_${agentId}`)).toHaveCount(0);
|
|
agentCreatedDelay.release();
|
|
|
|
const agentTab = page.getByTestId(`workspace-tab-agent_${agentId}`).first();
|
|
await expect(agentTab).toBeVisible({ timeout: 15_000 });
|
|
await expect
|
|
.poll(() => fetchActiveAgentTitle(workspace.client, agentId), { timeout: 10_000 })
|
|
.toBe(promptTitle);
|
|
await expect(agentTab).toContainText(promptTitle, { timeout: 15_000 });
|
|
await agentTab.click({ button: "right" });
|
|
await expect(page.getByTestId(`workspace-tab-context-agent_${agentId}-rename`)).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
await page.keyboard.press("Escape");
|
|
await expect(
|
|
page.getByText(/Loading agent title|Loading\.\.\./).filter({ visible: true }),
|
|
).toHaveCount(0);
|
|
|
|
await workspace.client.updateAgent(agentId, { name: generatedTitle });
|
|
await expect
|
|
.poll(() => fetchActiveAgentTitle(workspace.client, agentId), { timeout: 10_000 })
|
|
.toBe(generatedTitle);
|
|
await expect(page.getByRole("button", { name: generatedTitle }).first()).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
} finally {
|
|
agentCreatedDelay.release();
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|