Show workflows in the subagent track (#2933)
* feat(server): show provider workflows in subagent track Normalize workflow tasks into the provider-subagent contract so shared clients need no workflow awareness. Persisted nonterminal runs replay as failed because their original runtime can no longer deliver a terminal update. * fix(server): preserve workflow replay and tool cards * fix(server): suppress workflow notification cards * fix(server): replay workflow child timelines * fix(server): fail tasks when Claude exits * fix(server): preserve workflow replay parity * fix(server): fail tasks on Claude restart * fix(server): hide workflow child prompts on replay * fix: terminalize workflows on session reload * fix(server): own workflow notification routing * feat(server): show workflow results in detail pane * fix(server): preserve unrestored task notifications * fix(server): normalize replay task notifications
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { mkdtempSync, realpathSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { submitMessage } from "../support/helpers/composer";
|
||||
import { cleanupRewindFlow, launchAgent, type AgentHandle } from "../support/helpers/rewind-flow";
|
||||
import { openSubagentsTrack } from "../support/helpers/subagents";
|
||||
|
||||
process.env.CLAUDE_CODE_WORKFLOWS = "1";
|
||||
|
||||
const WORKFLOW_SCRIPT = path.resolve(__dirname, "../fixtures/claude-workflow/inspect-project.js");
|
||||
const REPO_ROOT = path.resolve(__dirname, "../../../..");
|
||||
const DESCRIPTION = "Inspect Paseo and explain what the project does";
|
||||
|
||||
test.describe("real Claude project-inspection workflow", () => {
|
||||
test.setTimeout(300_000);
|
||||
|
||||
test("shows the workflow's actual report in the generic detail pane", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-project-inspection-")));
|
||||
let handle: AgentHandle | undefined;
|
||||
|
||||
try {
|
||||
handle = await launchAgent({ page, provider: "claude", cwd, mode: "full-access" });
|
||||
await submitMessage(
|
||||
page,
|
||||
`Use Claude Code's Workflow tool exactly once with scriptPath ${JSON.stringify(
|
||||
WORKFLOW_SCRIPT,
|
||||
)} and args ${JSON.stringify({ repoPath: REPO_ROOT })}. Wait for completion, then reply with exactly PROJECT_INSPECTION_DONE.`,
|
||||
);
|
||||
|
||||
await expect(page.getByTestId("subagents-track-header")).toBeVisible({ timeout: 60_000 });
|
||||
await openSubagentsTrack(page);
|
||||
const row = page
|
||||
.getByTestId(/^subagents-track-row-/)
|
||||
.filter({ has: page.getByText(DESCRIPTION, { exact: true }) });
|
||||
await expect(row).toBeVisible({ timeout: 60_000 });
|
||||
await expect(row.getByRole("progressbar", { name: "Agent running" })).toHaveCount(0, {
|
||||
timeout: 180_000,
|
||||
});
|
||||
await expect(
|
||||
page.getByTestId("assistant-message").filter({ hasText: "PROJECT_INSPECTION_DONE" }).last(),
|
||||
).toBeVisible({ timeout: 180_000 });
|
||||
|
||||
await row.click();
|
||||
const panel = page.getByTestId("provider-subagent-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 30_000 });
|
||||
for (const heading of [
|
||||
"What Paseo is",
|
||||
"Who it is for",
|
||||
"How it works",
|
||||
"Repository structure",
|
||||
]) {
|
||||
await expect(panel.getByText(heading, { exact: true })).toBeVisible();
|
||||
}
|
||||
await expect(panel.getByText("export const meta", { exact: false })).toHaveCount(0);
|
||||
await expect(
|
||||
panel.getByText("Inspect the Paseo repository at", { exact: false }),
|
||||
).toHaveCount(0);
|
||||
await page.screenshot({ path: testInfo.outputPath("project-workflow-pane.png") });
|
||||
} finally {
|
||||
await cleanupRewindFlow({ handle, cwd });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { mkdtempSync, realpathSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "../support/fixtures";
|
||||
import {
|
||||
askClaudeToRunWorkflow,
|
||||
expectSingleWorkflowParentCard,
|
||||
expectWorkflowCompleted,
|
||||
expectWorkflowRunning,
|
||||
openWorkflowTimeline,
|
||||
releaseWorkflow,
|
||||
} from "../support/helpers/claude-workflow";
|
||||
import { cleanupRewindFlow, launchAgent, type AgentHandle } from "../support/helpers/rewind-flow";
|
||||
|
||||
// Claude Code currently gates the Workflow tool behind its own rollout. The real-provider test
|
||||
// opts in explicitly so it tests Paseo's integration rather than the account's rollout cohort.
|
||||
process.env.CLAUDE_CODE_WORKFLOWS = "1";
|
||||
|
||||
const WORKFLOW_SCRIPT = path.resolve(__dirname, "../fixtures/claude-workflow/one-child.js");
|
||||
|
||||
test.describe("real Claude workflow subagent row", () => {
|
||||
test.setTimeout(300_000);
|
||||
|
||||
test("shows a real Claude workflow running and completed through the generic subagent UI", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-claude-workflow-row-")));
|
||||
const gatePath = path.join(cwd, "release-workflow");
|
||||
let handle: AgentHandle | undefined;
|
||||
|
||||
try {
|
||||
handle = await launchAgent({ page, provider: "claude", cwd, mode: "full-access" });
|
||||
|
||||
await test.step("ask Claude to run the workflow", async () => {
|
||||
await askClaudeToRunWorkflow(handle!, WORKFLOW_SCRIPT, gatePath);
|
||||
await expectWorkflowRunning(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("workflow-running.png") });
|
||||
releaseWorkflow(gatePath);
|
||||
});
|
||||
|
||||
await test.step("see the workflow finish without leaving a running row", async () => {
|
||||
await expectWorkflowCompleted(page);
|
||||
await expectSingleWorkflowParentCard(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("workflow-completed.png") });
|
||||
});
|
||||
|
||||
await test.step("open the workflow through the existing provider-subagent pane", async () => {
|
||||
await openWorkflowTimeline(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("workflow-timeline.png") });
|
||||
});
|
||||
} finally {
|
||||
await cleanupRewindFlow({ handle, cwd });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export const meta = {
|
||||
name: "paseo-project-inspection",
|
||||
description: "Inspect Paseo and explain what the project does",
|
||||
whenToUse: "Paseo real-provider workflow detail QA",
|
||||
phases: [{ title: "Inspect", detail: "Read the product and architecture sources" }],
|
||||
};
|
||||
|
||||
phase("Inspect");
|
||||
let input = args;
|
||||
if (typeof input === "string") input = JSON.parse(input);
|
||||
const repoPath = input?.repoPath;
|
||||
if (typeof repoPath !== "string" || repoPath.length === 0) {
|
||||
throw new Error("repoPath is required");
|
||||
}
|
||||
const report = await agent(
|
||||
`Inspect the Paseo repository at ${JSON.stringify(repoPath)}.
|
||||
|
||||
Read README.md, docs/product.md, docs/architecture.md, and package.json. Do not edit anything. Return a concise plain-text report with exactly these headings:
|
||||
|
||||
What Paseo is
|
||||
Who it is for
|
||||
How it works
|
||||
Repository structure
|
||||
|
||||
Under each heading, give 1-3 concrete sentences grounded in those files. Mention the supported coding agents and the local-code/privacy model.`,
|
||||
{
|
||||
label: "project-inspector",
|
||||
phase: "Inspect",
|
||||
},
|
||||
);
|
||||
|
||||
return { report };
|
||||
@@ -0,0 +1,30 @@
|
||||
export const meta = {
|
||||
name: "paseo-workflow-row-qa",
|
||||
description: "Verify the workflow row lifecycle",
|
||||
whenToUse: "Paseo real-provider QA only",
|
||||
phases: [{ title: "Verify", detail: "one child returns a fixed marker" }],
|
||||
};
|
||||
|
||||
phase("Verify");
|
||||
let input = args;
|
||||
if (typeof input === "string") input = JSON.parse(input);
|
||||
const gatePath = input?.gatePath;
|
||||
if (typeof gatePath !== "string" || gatePath.length === 0) {
|
||||
throw new Error("gatePath is required");
|
||||
}
|
||||
const child = await agent(
|
||||
`First use Bash to run this command and wait for it to exit:
|
||||
while [ ! -f ${JSON.stringify(gatePath)} ]; do sleep 0.2; done
|
||||
|
||||
Then return exactly this JSON object and do nothing else: {"marker":"PASEO_WORKFLOW_ROW_OK"}`,
|
||||
{
|
||||
label: "workflow-row-child",
|
||||
phase: "Verify",
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["marker"],
|
||||
properties: { marker: { type: "string" } },
|
||||
},
|
||||
},
|
||||
);
|
||||
return { child };
|
||||
@@ -0,0 +1,75 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { submitMessage } from "./composer";
|
||||
import type { AgentHandle } from "./rewind-flow";
|
||||
import { openSubagentsTrack } from "./subagents";
|
||||
|
||||
export const WORKFLOW_ROW_DESCRIPTION = "Verify the workflow row lifecycle";
|
||||
export const WORKFLOW_ROW_MARKER = "PASEO_WORKFLOW_ROW_OK";
|
||||
|
||||
export async function askClaudeToRunWorkflow(
|
||||
handle: AgentHandle,
|
||||
scriptPath: string,
|
||||
gatePath: string,
|
||||
): Promise<void> {
|
||||
await submitMessage(
|
||||
handle.page,
|
||||
`Use Claude Code's Workflow tool exactly once with scriptPath ${JSON.stringify(
|
||||
scriptPath,
|
||||
)} and args ${JSON.stringify({ gatePath })}. Wait for its completion notification, then reply with exactly ${WORKFLOW_ROW_MARKER}.`,
|
||||
);
|
||||
await expect(handle.page.getByTestId("subagents-track-header")).toBeVisible({ timeout: 60_000 });
|
||||
await openSubagentsTrack(handle.page);
|
||||
}
|
||||
|
||||
export function releaseWorkflow(gatePath: string): void {
|
||||
writeFileSync(gatePath, "release\n", "utf8");
|
||||
}
|
||||
|
||||
export function workflowRow(page: Page) {
|
||||
// The parent transcript's Workflow tool card has the same accessible name. The track has no
|
||||
// separate landmark, so its established row test id is the only unambiguous scope.
|
||||
return page
|
||||
.getByTestId(/^subagents-track-row-/)
|
||||
.filter({ has: page.getByText(WORKFLOW_ROW_DESCRIPTION, { exact: true }) });
|
||||
}
|
||||
|
||||
export async function expectWorkflowRunning(page: Page): Promise<void> {
|
||||
const row = workflowRow(page);
|
||||
await expect(row).toBeVisible({ timeout: 60_000 });
|
||||
await expect(row).toContainText(WORKFLOW_ROW_DESCRIPTION);
|
||||
await expect(row).toContainText(/Workflow(?: · .+)?/);
|
||||
await expect(row.getByRole("progressbar", { name: "Agent running" })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function expectWorkflowCompleted(page: Page): Promise<void> {
|
||||
const row = workflowRow(page);
|
||||
await expect(row.getByRole("progressbar", { name: "Agent running" })).toHaveCount(0, {
|
||||
timeout: 120_000,
|
||||
});
|
||||
await expect(page.getByTestId("subagents-track-archive-finished")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("assistant-message").filter({ hasText: WORKFLOW_ROW_MARKER }).last(),
|
||||
).toBeVisible({ timeout: 120_000 });
|
||||
}
|
||||
|
||||
export async function expectSingleWorkflowParentCard(page: Page): Promise<void> {
|
||||
const parentToolCards = page.getByTestId("tool-call-badge");
|
||||
await expect(
|
||||
parentToolCards.filter({ has: page.getByText("Workflow", { exact: true }) }),
|
||||
).toHaveCount(1);
|
||||
await expect(parentToolCards.filter({ hasText: "Task Notification" })).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function openWorkflowTimeline(page: Page): Promise<void> {
|
||||
await workflowRow(page).click();
|
||||
const panel = page.getByTestId("provider-subagent-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("provider-subagent-pane-subtitle")).toHaveText(
|
||||
/Workflow(?: · .+)?/,
|
||||
);
|
||||
await expect(panel.getByText("Start chatting with this agent...", { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(panel.getByText(WORKFLOW_ROW_MARKER, { exact: true })).toBeVisible();
|
||||
}
|
||||
Reference in New Issue
Block a user