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
22 changes: 11 additions & 11 deletions apps/web/e2e/orchestration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ const pickChat = (page: Page) =>
const workspaceRow = (page: Page, workspaceId: string) =>
page.locator(`[data-testid="overview-workspace"][data-workspace-id="${workspaceId}"]`);

/** A finished worker's row under the overview's Done today. */
/** A finished worker's row under the overview's Done. */
const doneRow = (page: Page, threadId: string) =>
page.locator(`[data-testid="overview-done"][data-thread-id="${threadId}"]`);

/** A thread's row under the overview's Solo threads: none for a workspace's own threads. */
const soloRow = (page: Page, threadId: string) =>
page.locator(`[data-testid="overview-solo"][data-thread-id="${threadId}"]`);

/** Done today, which starts folded, opened. */
const unfoldDoneToday = async (page: Page) => {
/** Done, which starts folded, opened. */
const unfoldDone = async (page: Page) => {
const toggle = page.getByTestId("overview-section-doneToday").getByRole("button").first();
await expect(toggle).toHaveAttribute("aria-expanded", "false");
await toggle.click();
Expand Down Expand Up @@ -96,11 +96,11 @@ test("an orchestrator and its worker are marked, and their transcripts show the
const made = await simulate(page);

// Their home is the workspace's line under Orchestrators, by its name; the worker, reported
// done, is under Done today, which names its workspace. Solo threads lists neither.
// done, is under Done, which names its workspace. Solo threads lists neither.
const workspace = workspaceRow(page, made.workspaceId);
await expect(workspace.getByTestId("overview-workspace-row")).toHaveAccessibleName(/^api, /);
await expect(workspace).toContainText("No workers now.");
await unfoldDoneToday(page);
await unfoldDone(page);
await expect(doneRow(page, made.workerThreadId)).toHaveAccessibleName(
/^Return rate-limit headers, api · done, /,
);
Expand Down Expand Up @@ -230,9 +230,9 @@ test("a worker's menu goes to its orchestrator, removing makes it a plain thread
await page.goto("/t/t-flaky");
await expect(threadRow(page, "t-flaky")).toBeVisible();
const made = await simulate(page);
// The worker reported done, so its row is under Done today, and its menu is its thread's More….
// The worker reported done, so its row is under Done, and its menu is its thread's More….
const workerRow = doneRow(page, made.workerThreadId);
await unfoldDoneToday(page);
await unfoldDone(page);
await workerRow.click();
await expect(page).toHaveURL(new RegExp(`/t/${made.workerThreadId}$`));

Expand Down Expand Up @@ -762,10 +762,10 @@ test("an orchestrator shows no PR of its own, and its PR tab points at the Board
await expect(page.getByTestId("pr-row")).toBeVisible();
});

test("finished work is under a folded Done today, and a merged worker offers its cleanup", async ({
test("finished work is under a folded Done, and a merged worker offers its cleanup", async ({
page,
}) => {
// Midday, so the work the mock finished hours ago is still today's whatever the hour it runs.
// Midday, so the work the mock finished hours ago is in the week whatever the hour it runs.
await page.clock.install({ time: new Date("2026-10-21T12:00:00") });
await page.goto("/t/t-flaky");
await expect(threadRow(page, "t-flaky")).toBeVisible();
Expand All @@ -776,9 +776,9 @@ test("finished work is under a folded Done today, and a merged worker offers its
});

const [merged, , , shipped] = made.merged;
// Finished work is under Done today, which starts folded: the delivered worker, and the merged
// Finished work is under Done, which starts folded: the delivered worker, and the merged
// one of `mail-trigger`, a feature still going.
await unfoldDoneToday(page);
await unfoldDone(page);
await expect(doneRow(page, made.delivered)).toHaveAccessibleName(
/^Changelog lint script, agent-platform · done, /,
);
Expand Down
61 changes: 50 additions & 11 deletions apps/web/e2e/overview-extras.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { expect, test, type Page } from "@playwright/test";
import type { MockOneThreadSimulation } from "../src/api/mock-asks";
import { pressFirstWorkspaceKey } from "./thread-row";

// The overview sidebar's extras: Pull requests and Done
// today, folded until opened, the usage meters and worker slots at its foot, off by default, ⌘J's jump into its
// workers, and Settings → General's Sidebar sections. Each is checked against what the right
// panel's PR lists, ⌘J's list, the title bar's meter and the ledger say. Mock state lives in memory, so every
// step after the seed stays inside the app, bar the reload the settings are kept across.
// The overview sidebar's extras: Pull requests and Done, folded until opened, the usage meters and
// worker slots at its foot, off by default, ⌘J's jump into its workers, and Settings → General's
// Sidebar sections. Each is checked against what the right panel's PR lists, ⌘J's list, the title
// bar's meter and the ledger say. Mock state lives in memory, so every step after the seed stays
// inside the app, bar the reload the settings are kept across.

const MOCK_SETTINGS_KEY = "gram.mock.settings";

Expand Down Expand Up @@ -189,15 +189,21 @@ test("Pull requests stays folded until opened, kept across a reload, and leaves
await expect(kinds.and(page.locator('[data-kind="yours"]')).first()).toBeVisible();
});

test("Done today starts folded and has ⌘J's Done today rows, opening one as ⌘J does", async ({
test("Done starts folded and has the week of ⌘J's Done rows by day, opening one as ⌘J does", async ({
page,
}) => {
const made = await start(page);
const title = sectionTitle(page, "doneToday");
await expect(title).toHaveAttribute("aria-expanded", "false");
await expect(title).toHaveAccessibleName(/^Done, \d+$/);
await title.click();
const rows = section(page, "doneToday").getByTestId("overview-done");
await expect(rows.first()).toBeVisible();
// Under their days, today's first.
const days = section(page, "doneToday").getByTestId("overview-done-day");
await expect(days.first()).toHaveText("Today");
for (const day of await days.allTextContents())
expect(day).toMatch(/^(Today|Yesterday|\w{3} \d{1,2} \w{3})$/);

// ⌘J's own list, with the overview hidden so ⌘J opens it.
await page.keyboard.press("ControlOrMeta+KeyB");
Expand All @@ -206,7 +212,7 @@ test("Done today starts folded and has ⌘J's Done today rows, opening one as
const list = page.getByTestId("workers-list");
await expect(list).toBeVisible();
const fromList = await list
.getByTestId("workers-section-doneToday")
.getByTestId("workers-section-done")
.getByTestId("worker-row")
.evaluateAll((els) => els.map((el) => el.getAttribute("data-thread-id")));
expect(fromList).toContain(made.doneToday);
Expand All @@ -215,26 +221,59 @@ test("Done today starts folded and has ⌘J's Done today rows, opening one as
await page.keyboard.press("ControlOrMeta+KeyB");
await expect(overview(page)).toBeVisible();

// The overview keeps the last seven days of them, in ⌘J's order; ⌘J has the older ones too.
const here = rows.and(page.locator(`[data-workspace-id="${made.workspaceId}"]`));
await expect(here).toHaveCount(fromList.length);
const fromOverview = await here.evaluateAll((els) =>
els.map((el) => el.getAttribute("data-thread-id")),
);
expect(fromOverview.toSorted()).toEqual(fromList.toSorted());
expect(fromOverview).toEqual(fromList.filter((id) => fromOverview.includes(id)));
expect(fromOverview).toContain(made.doneToday);
expect(fromOverview).toContain(made.earlier[0]);
expect(fromList).toEqual(expect.arrayContaining([...made.earlier]));
expect(fromOverview).not.toContain(made.earlier[1]);
expect(fromOverview).not.toContain(made.earlier[2]);

const merged = rows.and(page.locator(`[data-thread-id="${made.doneToday}"]`));
await expect(merged).toHaveAttribute("data-merged", "true");
await expect(merged.getByTestId("overview-done-time")).toHaveText(/^\d{2}:\d{2}$/);
// One line, grey until hovered: what became of it is on its hover and in its name.
await expect(merged).toHaveAttribute("title", /^agent-platform · merged api#\d+$/);
await expect(merged).toHaveAccessibleName(/, agent-platform · merged api#\d+, \d{2}:\d{2}$/);
await expect(merged).toHaveAccessibleName(
/, agent-platform · merged api#\d+, Today \d{2}:\d{2}$/,
);
await expect(merged).toHaveCSS("filter", /grayscale\(1\)/);
await merged.hover();
await expect(merged).not.toHaveCSS("filter", /grayscale\(1\)/);
await merged.click();
await expect(page).toHaveURL(new RegExp(`/t/${made.doneToday}$`));
});

test("Done shows its first ten rows, and Show all opens ⌘J on its Done", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/t/t-flaky");
await page.getByLabel("Message", { exact: true }).waitFor();
// Archived groups: more finished this week than the column shows.
await page.evaluate(() => window.gramMock!.simulateArchivedGroups());
await pressFirstWorkspaceKey(page);
const title = sectionTitle(page, "doneToday");
await expect(title).toHaveAccessibleName(/^Done, \d+$/);
const count = Number((await title.getAttribute("aria-label"))!.split(", ")[1]);
expect(count).toBeGreaterThan(10);
// Folded, it says how many it holds; open, it shows ten of them and the way to the rest.
await expect(title).toContainText(String(count));
await title.click();
const done = section(page, "doneToday");
await expect(done.getByTestId("overview-done")).toHaveCount(10);
const more = done.getByTestId("overview-done-more");
await expect(more).toHaveText(/^Show all in Workers \(.+\)$/);
await more.click();
const list = page.getByTestId("workers-list");
await expect(list).toBeVisible();
const listed = list.getByTestId("workers-section-done");
await expect(listed.getByTestId("workers-day").first()).toBeInViewport();
await expect(listed.getByTestId("worker-row").first()).toHaveAttribute("aria-selected", "true");
});

test("the foot is off by default, and the orchestrator's line keeps the slots on its hover", async ({
page,
}) => {
Expand Down Expand Up @@ -378,7 +417,7 @@ test("Settings' Sidebar sections hides and shows each section, kept across a rel
const labels = {
solo: "Solo threads",
pullRequests: "Pull requests",
doneToday: "Done today",
doneToday: "Done",
usage: "Usage and slots",
} as const;
await expect(group.getByRole("checkbox")).toHaveCount(4);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/e2e/overview-sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,9 @@ test("arrows and End skip a folded section's rows", async ({ page }) => {
overview(page).getByTestId(`overview-section-${id}`).getByRole("button", { name });
const orchestrators = head("orchestrators", /Orchestrators/);
const soloHead = head("solo", /Solo threads/);
// Pull requests and Done today come after Solo threads, folded by default.
// Pull requests and Done come after Solo threads, folded by default.
const prsHead = head("pullRequests", /Pull requests/);
const doneHead = head("doneToday", /Done today/);
const doneHead = head("doneToday", /^Done, /);
await expect(doneHead).toHaveAttribute("aria-expanded", "false");
await orchestrators.click();
await expect(orchestrators).toHaveAttribute("aria-expanded", "false");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/perf-views.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const TRACKED = [
"BoardHeader",
"AskStack",
"WorkersButton",
"OverviewDoneToday",
"OverviewDone",
"NeedsYouShortcut",
"NeedsYou",
"AwayNotifications",
Expand Down
16 changes: 11 additions & 5 deletions apps/web/e2e/workers-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,20 @@ test("⌘J lists every worker by section and feature, and opens one in two steps
.filter({ has: page.locator(`[data-thread-id="${made.doneToday}"]`) }),
).toHaveCount(0);

// Done today and Earlier are sections of the same list, a click away without a tab.
const doneToday = page.getByTestId("workers-section-doneToday");
// Done is a section of the same list, a click away without a tab, by the day each finished:
// today's first, and every earlier day ⌘J still has, past the overview's week too.
const done = page.getByTestId("workers-section-done");
await expect(
doneToday.locator(`[data-thread-id="${made.doneToday}"]`).getByTestId("worker-now"),
done.locator(`[data-thread-id="${made.doneToday}"]`).getByTestId("worker-now"),
).toHaveText(/^merged \d{2}:\d{2}$/);
const earlier = page.getByTestId("workers-section-earlier");
for (const id of made.earlier)
await expect(earlier.locator(`[data-thread-id="${id}"]`)).toBeAttached();
await expect(done.locator(`[data-thread-id="${id}"]`)).toBeAttached();
const days = done.getByTestId("workers-day");
await expect(days.first()).toHaveText("Today");
// Three earlier days at least: 3, 9 and 12 days back.
await expect.poll(() => days.count()).toBeGreaterThanOrEqual(4);
for (const day of await days.allTextContents())
expect(day).toMatch(/^(Today|Yesterday|\w{3} \d{1,2} \w{3})$/);

// The search box filters every section.
const search = page.getByTestId("workers-search");
Expand Down
32 changes: 26 additions & 6 deletions apps/web/pr-shots/shots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1951,7 +1951,7 @@ export const SHOTS: readonly Shot[] = [
},
{
name: "overview-sidebar-extras",
title: "The overview at 1440 px with Pull requests and Done today opened",
title: "The overview at 1440 px with Pull requests and Done opened",
open: async (page) => {
await openOverviewHome(page, 1440, { panel: false });
for (const id of ["pullRequests", "doneToday"]) {
Expand All @@ -1962,13 +1962,30 @@ export const SHOTS: readonly Shot[] = [
if ((await title.getAttribute("aria-expanded")) === "false") await title.click();
}
await page.getByTestId("overview-done").first().waitFor();
// Done today sits last in the column.
// Done sits last in the column.
await page.getByTestId("overview-done").last().scrollIntoViewIfNeeded();
// Off the column, so no row shows its hover.
await page.mouse.move(1200, 400);
await page.waitForTimeout(400);
},
},
{
name: "overview-done-week",
title: "The overview's Done opened on a busy week: its days, ten rows and Show all",
open: async (page) => {
await seedMockSettings(page, { panel: { open: false } });
await openOrchestratorOf(page, () => window.gramMock!.simulateArchivedGroups());
const title = page
.getByTestId("overview-section-doneToday")
.getByRole("heading")
.getByRole("button");
if ((await title.getAttribute("aria-expanded")) === "false") await title.click();
await page.getByTestId("overview-done").last().scrollIntoViewIfNeeded();
await page.mouse.move(1200, 400);
await page.waitForTimeout(400);
},
element: (page) => page.getByTestId("overview-sidebar"),
},
{
name: "overview-sidebar-hover",
title: "A worker's hover card in the overview: title, station, now line, first PR and time",
Expand Down Expand Up @@ -2146,12 +2163,15 @@ export const SHOTS: readonly Shot[] = [
},
{
name: "workers-list-earlier",
title: "⌘J scrolled to Earlier, with its search",
title: "⌘J scrolled to its earlier days (Earlier before Done took it in), with its search",
modes: ["dark"],
open: async (page) => {
await openWorkersScenario(page);
await pressWorkersList(page);
await page.getByTestId("workers-section-earlier").scrollIntoViewIfNeeded();
// The last day's heading in Done; before it, the Earlier section.
const days = page.getByTestId("workers-day");
if ((await days.count()) > 0) await days.last().scrollIntoViewIfNeeded();
else await page.getByTestId("workers-section-earlier").scrollIntoViewIfNeeded();
await page.getByTestId("workers-search").waitFor();
},
element: (page) => page.getByTestId("workers-list"),
Expand Down Expand Up @@ -2241,12 +2261,12 @@ export const SHOTS: readonly Shot[] = [
},
{
name: "workers-list-done-today",
title: "⌘J's Done today, with a worker whose worktree was cleaned up",
title: "⌘J's Done (Done today before), with a worker whose worktree was cleaned up",
modes: ["dark"],
open: async (page) => {
await openReachingScenario(page);
await pressWorkersList(page);
await page.getByTestId("workers-section-doneToday").scrollIntoViewIfNeeded();
await page.getByTestId(/^workers-section-(done|doneToday)$/).scrollIntoViewIfNeeded();
},
element: (page) => page.getByTestId("workers-list"),
},
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/api/mock-workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

import type { MockFinishedSimulation, MockOrchestrationHost } from "./mock-orchestration";

// The ⌘J Workers list scenario: the finished-work board with workers in every tab the list shows.
// The ⌘J Workers list scenario: the finished-work board with workers in both of the list's sections.
// A worker running live with a long transcript to scroll, one that asked a question, one merged
// today, and three finished on earlier days. Two of them carry reports the user hasn't seen, one
// with the helper's catch-up armed and one without, which falls back to its report's summary.
Expand All @@ -34,7 +34,7 @@ export interface MockWorkersSimulation extends MockFinishedSimulation {
readonly live: ThreadId;
/** Asked the orchestrator a question it is waiting on; its unseen report has no catch-up. */
readonly asking: ThreadId;
/** Merged within the last hour, so in Done today whatever the time. */
/** Merged within the last hour, so under Done's Today whatever the time. */
readonly doneToday: ThreadId;
/** Merged or archived days ago. */
readonly earlier: readonly ThreadId[];
Expand Down Expand Up @@ -210,7 +210,7 @@ export async function simulateWorkersList(
at: now - 20 * MINUTE,
});

// Done today: merged within the hour, or just after midnight when it is earlier than that.
// Today: merged within the hour, or just after midnight when it is earlier than that.
const midnight = new Date(now);
midnight.setHours(0, 0, 0, 0);
const todayAt = Math.max(now - 40 * MINUTE, midnight.getTime() + MINUTE);
Expand All @@ -223,7 +223,7 @@ export async function simulateWorkersList(
at: Math.min(todayAt, now) - 5 * MINUTE,
});

// Earlier: finished on other days.
// Earlier days: one in the overview's week, two before it that only ⌘J lists.
const uv = await spawn("tooling", "Migrate to uv", "migrate-uv");
merged(uv, 377, now - 3 * DAY);
const py39 = await spawn("tooling", "Drop Python 3.9", "drop-py39");
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/orchestration/narrow-subscriptions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import { useThreadsStore } from "@/stores/threads";
import { useBoardWaiting } from "./board-waiting";
import { useRailFacts } from "./feature-sheet";
import { resetDerivedAsks } from "./needs-you-store";
import { OverviewDoneToday } from "./overview-extras";
import { OverviewDone } from "./overview-extras";
import { NeedsYouSource } from "./use-needs-you";
import { useWorkspaces } from "./use-orchestration";
import { WorkersButton } from "./workers-list";

// The always-mounted views that derive a little from the whole thread map re-render only when what
// they derive changes, not on every change to any thread: the title bar's Workers button, the
// overview's Done today, the Board's chips and its Waiting on you.
// overview's Done, the Board's chips and its Waiting on you.

const route = vi.hoisted(() => ({ workspaceId: "" }));
vi.mock("@tanstack/react-router", async (importOriginal) => ({
Expand Down Expand Up @@ -60,7 +60,7 @@ function Window() {
<>
<NeedsYouSource />
{counted("workers-button", <WorkersButton />)}
{counted("done-today", <OverviewDoneToday />)}
{counted("done", <OverviewDone />)}
<Board />
</>
);
Expand Down
Loading
Loading