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
54 changes: 54 additions & 0 deletions apps/web/e2e/support/conversation-work-cell-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ export function installActiveChannelWorkCellFixture(state: MockState): {
return result;
}

export function installExplorationWorkCellFixture(state: MockState, readCount = 3): {
readonly runId: string;
readonly threadId: string;
} {
const result = installChannelWorkCellFixture(state, "exploration");
const readPaths = [
"apps/web/src/lib/conversation-work-cell.ts",
"apps/web/src/lib/workflow-progress.ts",
"apps/web/src/shared/conversation/blocks/ConversationWorkCell.tsx",
"packages/conversation-ui-core/src/activity-projection.ts",
"packages/conversation-ui-core/src/command-batching.ts",
"apps/web/src/styles/shared/conversation-work-cell-activity.css",
];
state.executionActions = [
...Array.from({ length: readCount }, (_, index) => readAction(
index < 3 ? ["one", "two", "three"][index]! : `item-${index + 1}`,
result.threadId,
result.runId,
readPaths[index % readPaths.length]!,
index,
)),
];
return result;
}

export function installNormalWorkCellFixture(state: MockState): string {
const title = "Focused normal WorkCell";
const threadId = "thread_ui2_normal_work_cell";
Expand Down Expand Up @@ -235,6 +260,35 @@ function completedCommandActions(threadId: string, runId: string, suffix: string
];
}

function readAction(
id: string,
threadId: string,
runId: string,
path: string,
index: number,
): MockExecutionAction {
const actionTime = new Date(Date.parse(startedAt) + ((index + 1) * 8_000)).toISOString();
return {
id: `execution_action_read_${id}`,
workspaceId,
threadId,
runId,
toolCallId: `tool_read_${id}`,
actionType: "read",
risk: "low",
status: "succeeded",
title: "Read file",
summary: `Read ${path}`,
targetRefs: [{ type: "file", path, label: path }],
previewArtifactIds: [],
resultArtifactIds: [],
createdAt: actionTime,
updatedAt: actionTime,
executedAt: actionTime,
completedAt: actionTime,
};
}

function commandAction(
id: string,
threadId: string,
Expand Down
69 changes: 69 additions & 0 deletions apps/web/e2e/ui2-conversation-work-cell-activity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import {
installActiveChannelWorkCellFixture,
installChannelWorkCellFixture,
installExplorationWorkCellFixture,
installNormalWorkCellFixture,
installTeammateWorkCellFixture,
} from "./support/conversation-work-cell-fixture";
Expand Down Expand Up @@ -195,6 +196,74 @@ test("Normal conversation keeps the completed command batch in the shared WorkCe
expect(runtimeErrors).toEqual([]);
});

test("Completed WorkCell collapses adjacent read actions into an expandable exploration batch", async ({ page }) => {
await prepareSurface(page, { theme: "light", width: 1440, height: 960 });
const runtimeErrors = collectRuntimeErrors(page);
const state = createBaseState({ approvalStatus: "expired" });
const { threadId } = installExplorationWorkCellFixture(state);
await mockDaemon(page, state);

await page.goto("/");
await openEngineeringChannel(page);

const feed = page.getByLabel("Channel message feed");
const workCell = feed.locator(`.conversation-work-cell[data-thread-id="${threadId}"]`);
await expect(workCell).toBeVisible();
await expect(workCell.locator(".conversation-operation-shell-status")).toContainText("Read 3 items");
await expandOperationShell(workCell);

const batch = workCell.locator(".conversation-exploration-batch");
const trigger = batch.locator(".conversation-exploration-batch-header");
await expect(trigger).toHaveAttribute("aria-expanded", "false");
await trigger.focus();
await trigger.press("Space");
await expect(trigger).toHaveAttribute("aria-expanded", "true");
await expect(batch.locator(".conversation-exploration-batch-list .conversation-work-cell-row")).toHaveCount(3);
await expectContained(page, [feed, workCell, batch]);
expect(runtimeErrors).toEqual([]);
});

test("Dense exploration activity stays scrollable after ten atomic rows", async ({ page }) => {
await prepareSurface(page, { theme: "light", width: 1440, height: 600 });
const runtimeErrors = collectRuntimeErrors(page);
const state = createBaseState({ approvalStatus: "expired" });
const { threadId } = installExplorationWorkCellFixture(state, 11);
await mockDaemon(page, state);

await page.goto("/");
await openEngineeringChannel(page);

const feed = page.getByLabel("Channel message feed");
const workCell = feed.locator(`.conversation-work-cell[data-thread-id="${threadId}"]`);
await expect(workCell).toBeVisible();
await expect(workCell.locator(".conversation-operation-shell-status")).toContainText("Read 11 items");
await expandOperationShell(workCell);

const stream = workCell.locator('[data-conversation-work-stream="primary"]');
await expect(stream).toHaveAttribute("data-activity-density", "dense");
await expect(stream).toHaveCSS("overflow-y", "auto");
await expect.poll(() => stream.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
}))).toEqual(expect.objectContaining({
scrollHeight: expect.any(Number),
}));
const dimensions = await stream.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
}));
expect(dimensions.scrollHeight).toBeGreaterThan(dimensions.clientHeight);

const batch = workCell.locator(".conversation-exploration-batch");
const trigger = batch.locator(".conversation-exploration-batch-header");
await trigger.focus();
await trigger.press("Space");
await expect(trigger).toHaveAttribute("aria-expanded", "true");
await expect(batch.locator(".conversation-exploration-batch-list .conversation-work-cell-row")).toHaveCount(11);
await expectContained(page, [feed, workCell, stream, batch]);
expect(runtimeErrors).toEqual([]);
});

test("Teammate conversation keeps command activity contained in dark reduced motion", async ({ page }) => {
await prepareSurface(page, { theme: "dark", width: 1280, height: 800, reducedMotion: true });
const runtimeErrors = collectRuntimeErrors(page);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1048,8 +1048,11 @@ export const enUS = {
"Editing {target}": "Editing {target}",
"Ran command": "Ran command",
"Ran {count} commands": "Ran {count} commands",
"Read {count} items": "Read {count} items",
"Expand command batch": "Expand command batch",
"Collapse command batch": "Collapse command batch",
"Expand read records": "Expand read records",
"Collapse read records": "Collapse read records",
"Running command": "Running command",
"Searched": "Searched",
"Searched {target}": "Searched {target}",
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3462,8 +3462,11 @@ export const zhCN = {
"Editing {target}": "正在编辑 {target}",
"Ran command": "已运行命令",
"Ran {count} commands": "已运行 {count} 个命令",
"Read {count} items": "已读取 {count} 项",
"Expand command batch": "展开命令批次",
"Collapse command batch": "折叠命令批次",
"Expand read records": "展开读取记录",
"Collapse read records": "折叠读取记录",
"Running command": "正在运行命令",
"Searched": "已搜索",
"Searched {target}": "已搜索 {target}",
Expand Down
Loading