The subagents track and the task list were stacked cards above the composer. Each one pushed the composer further down the pane and painted an opaque band the timeline stopped at, and both spent a full row on a count that fits in a pill. Both are now pills that open a panel — a popover on wide screens, a sheet on compact ones, from the existing menu engine rather than a fourth floating-panel implementation. The row frame is shared, so a subagent row and a task row sit on the same rail whatever their contents. The bar mounts inside the transcript's animated container and pins itself to the bottom edge, so scrolled content passes under the pills instead of stopping above them. It stays a child of that container rather than a sibling floating over the composer: on Android a view outside its parent's bounds paints but takes no touches, and every pill is pressable. The container carries the same keyboard transform the composer does, so the pills track the composer's top edge when the keyboard moves. The tracker state moves out of the composer with it. None of it was composer state; it lived there because that is where it used to render.
148 lines
6.0 KiB
TypeScript
148 lines
6.0 KiB
TypeScript
import { mkdtempSync, realpathSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { test, expect } from "../support/fixtures";
|
|
import {
|
|
cleanupRewindFlow,
|
|
launchAgent,
|
|
sendMessage,
|
|
type AgentHandle,
|
|
type RewindFlowProvider,
|
|
} from "../support/helpers/rewind-flow";
|
|
import { openSubagentsTrack } from "../support/helpers/subagents";
|
|
|
|
interface ProviderSubagentCase {
|
|
provider: RewindFlowProvider;
|
|
sentinel: string;
|
|
expectedName: string;
|
|
expectedSubtitle?: RegExp;
|
|
expectsUserMessage?: boolean;
|
|
prompt: string;
|
|
providerConfig?: Parameters<typeof launchAgent>[0]["providerConfig"];
|
|
}
|
|
|
|
const cases: ProviderSubagentCase[] = [
|
|
{
|
|
provider: "claude",
|
|
sentinel: "CLAUDE_CHILD_SENTINEL",
|
|
expectedName: "sentinel_child",
|
|
providerConfig: { model: "opus" },
|
|
prompt:
|
|
'Use Claude Code\'s native Task tool exactly once. Set its subagent_type input to "Explore" and its name input to "sentinel_child". Ask it to reply with exactly CLAUDE_CHILD_SENTINEL and do nothing else. Wait for it, then reply ROOT_DONE. Do not use Paseo tools.',
|
|
},
|
|
{
|
|
provider: "codex",
|
|
sentinel: "CODEX_CHILD_SENTINEL",
|
|
expectedName: "Sentinel child",
|
|
providerConfig: { providerOptions: { features: { multi_agent_v2: true } } },
|
|
prompt:
|
|
'Use the native collaboration.spawn_agent tool exactly once with task_name "sentinel_child" and fork_turns "none". Ask it to reply with exactly CODEX_CHILD_SENTINEL and do nothing else. Wait for it with collaboration.wait_agent, then reply ROOT_DONE. Do not use Paseo tools.',
|
|
},
|
|
{
|
|
provider: "opencode",
|
|
sentinel: "OPENCODE_CHILD_SENTINEL",
|
|
expectedName: "Verify OpenCode descriptor",
|
|
expectedSubtitle: /explore · gpt-5\.4(?: · [^\n·]+)? · \d+(?:\.\d+)?k? tokens/i,
|
|
expectsUserMessage: false,
|
|
providerConfig: { model: "openai/gpt-5.4" },
|
|
prompt:
|
|
'Use the task tool exactly once with description "Verify OpenCode descriptor" and the explore subagent. Ask it to reply with exactly OPENCODE_CHILD_SENTINEL and do nothing else. Wait for it, then reply ROOT_DONE.',
|
|
},
|
|
];
|
|
|
|
test.describe("real provider subagent timelines", () => {
|
|
test.setTimeout(600_000);
|
|
|
|
for (const scenario of cases) {
|
|
test(`${scenario.provider} exposes native child output from the subagent track`, async ({
|
|
page,
|
|
}, testInfo) => {
|
|
const cwd = realpathSync(
|
|
mkdtempSync(path.join(tmpdir(), `paseo-provider-subagent-${scenario.provider}-`)),
|
|
);
|
|
let handle: AgentHandle | undefined;
|
|
|
|
try {
|
|
handle = await launchAgent({
|
|
page,
|
|
provider: scenario.provider,
|
|
cwd,
|
|
mode: "full-access",
|
|
providerConfig: scenario.providerConfig,
|
|
});
|
|
await sendMessage(handle, scenario.prompt);
|
|
await openSubagentsTrack(page);
|
|
|
|
const rows = page.locator('[data-testid^="subagents-track-row-"]');
|
|
await expect(rows).toHaveCount(1, { timeout: 60_000 });
|
|
await expect(rows.first()).toContainText(scenario.expectedName);
|
|
if (scenario.expectedSubtitle) {
|
|
await expect(rows.first()).toContainText(scenario.expectedSubtitle);
|
|
const desktopTrackScreenshot = testInfo.outputPath(
|
|
`${scenario.provider}-subagent-track-desktop.png`,
|
|
);
|
|
await page.screenshot({ path: desktopTrackScreenshot });
|
|
await testInfo.attach(`${scenario.provider} subagent track desktop`, {
|
|
path: desktopTrackScreenshot,
|
|
contentType: "image/png",
|
|
});
|
|
}
|
|
await rows.first().click();
|
|
|
|
const panel = page.getByTestId("provider-subagent-panel");
|
|
await expect(panel).toBeVisible({ timeout: 30_000 });
|
|
if (scenario.expectedSubtitle) {
|
|
await expect(page.getByTestId("provider-subagent-pane-subtitle")).toHaveText(
|
|
scenario.expectedSubtitle,
|
|
);
|
|
const desktopScreenshot = testInfo.outputPath(
|
|
`${scenario.provider}-subagent-desktop.png`,
|
|
);
|
|
await page.screenshot({ path: desktopScreenshot });
|
|
await testInfo.attach(`${scenario.provider} subagent desktop`, {
|
|
path: desktopScreenshot,
|
|
contentType: "image/png",
|
|
});
|
|
await page.setViewportSize({ width: 494, height: 862 });
|
|
await expect(page.getByTestId("provider-subagent-pane-subtitle")).toHaveText(
|
|
scenario.expectedSubtitle,
|
|
);
|
|
const compactScreenshot = testInfo.outputPath(
|
|
`${scenario.provider}-subagent-compact.png`,
|
|
);
|
|
await page.screenshot({ path: compactScreenshot });
|
|
await testInfo.attach(`${scenario.provider} subagent compact`, {
|
|
path: compactScreenshot,
|
|
contentType: "image/png",
|
|
});
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
}
|
|
if (scenario.expectsUserMessage !== false) {
|
|
await expect(
|
|
panel.getByTestId("user-message").filter({ hasText: scenario.sentinel }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
}
|
|
await expect(
|
|
panel.getByTestId("assistant-message").filter({ hasText: scenario.sentinel }),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
await expect(
|
|
panel.getByText("Start chatting with this agent...", { exact: true }),
|
|
).toHaveCount(0);
|
|
|
|
await page.getByTestId(`workspace-tab-agent_${handle.agentId}`).first().click();
|
|
await expect(
|
|
page.getByTestId("assistant-message").filter({ hasText: "ROOT_DONE" }).last(),
|
|
).toBeVisible({ timeout: 60_000 });
|
|
// Opening the subagent's tab closed the panel with the parent's pane.
|
|
await openSubagentsTrack(page);
|
|
const archiveFinished = page.getByTestId("subagents-track-archive-finished");
|
|
await expect(archiveFinished).toBeVisible({ timeout: 30_000 });
|
|
await archiveFinished.click();
|
|
await expect(rows).toHaveCount(0, { timeout: 30_000 });
|
|
} finally {
|
|
await cleanupRewindFlow({ handle, cwd });
|
|
}
|
|
});
|
|
}
|
|
});
|