Skip to content

Commit 3591e2a

Browse files
committed
feat(ui-preview): add README showcase scenes with PanelHeader inline meta
Add three new UI preview scenes (readme-desktop-hero, readme-desktop-review, readme-mobile-progress) and extend PanelHeader with metaPlacement prop to support inline layout. Update tests and CSS accordingly.
1 parent e28a3e5 commit 3591e2a

9 files changed

Lines changed: 850 additions & 38 deletions

File tree

packages/web/src/features/agent-panes/components/session-card.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,30 @@ describe("SessionCard", () => {
265265
expect(screen.getByText("Running")).toHaveClass("badge", "badge-green", "session-state-badge");
266266
});
267267

268+
it("renders the session title and badges inside one header row", () => {
269+
const { store } = createSessionStore({
270+
terminalId: "term-live",
271+
state: "idle",
272+
endedAt: undefined,
273+
});
274+
275+
const { container } = render(
276+
<Provider store={store}>
277+
<SessionCard sessionId="sess_123456" />
278+
</Provider>
279+
);
280+
281+
const headerRow = container.querySelector(".panel-header__title-row");
282+
const inlineMeta = container.querySelector(".panel-header__meta--inline");
283+
284+
expect(headerRow).not.toBeNull();
285+
expect(headerRow).toContainElement(screen.getByText("SESSION-56"));
286+
expect(headerRow).toContainElement(screen.getByText("Codex"));
287+
expect(headerRow).toContainElement(screen.getByText("Idle"));
288+
expect(inlineMeta).not.toBeNull();
289+
expect(headerRow).toContainElement(inlineMeta as HTMLElement);
290+
});
291+
268292
it("renders a header accessory on the right side of the session header", () => {
269293
const { store } = createSessionStore({
270294
terminalId: "term-live",

packages/web/src/features/shared/components/panel-header.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,26 @@ describe("PanelHeader", () => {
4141
expect(within(status as HTMLElement).getByText("Modified")).toBeInTheDocument();
4242
expect(screen.queryByRole("button")).not.toBeInTheDocument();
4343
});
44+
45+
it("can render meta inline inside the title row when requested", () => {
46+
render(
47+
<PanelHeader
48+
title="SESSION-20"
49+
meta={<span>Codex</span>}
50+
status={<span>Online</span>}
51+
metaPlacement="inline"
52+
/>
53+
);
54+
55+
const header = document.querySelector(".panel-header");
56+
const titleRow = header?.querySelector(".panel-header__title-row");
57+
const inlineMeta = header?.querySelector(".panel-header__meta--inline");
58+
59+
expect(header).toHaveClass("panel-header", "panel-header--inline-meta");
60+
expect(titleRow).not.toBeNull();
61+
expect(inlineMeta).not.toBeNull();
62+
expect(titleRow).toContainElement(screen.getByText("SESSION-20"));
63+
expect(titleRow).toContainElement(screen.getByText("Codex"));
64+
expect(titleRow).toContainElement(inlineMeta as HTMLElement);
65+
});
4466
});

packages/web/src/styles/components.css

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7198,6 +7198,10 @@ textarea.input {
71987198
min-width: 0;
71997199
}
72007200

7201+
.panel-header--inline-meta .panel-header__title-row {
7202+
flex-wrap: nowrap;
7203+
}
7204+
72017205
.panel-header__title {
72027206
min-width: 0;
72037207
overflow: hidden;
@@ -7223,6 +7227,10 @@ textarea.input {
72237227
min-width: 0;
72247228
}
72257229

7230+
.panel-header__meta--inline {
7231+
flex: 0 0 auto;
7232+
}
7233+
72267234
.panel-header__actions {
72277235
display: inline-flex;
72287236
align-items: center;
@@ -7428,6 +7436,11 @@ textarea.input {
74287436
border-bottom-color: var(--border-focus);
74297437
}
74307438

7439+
.session-card.session-card--active > .panel-header {
7440+
background: color-mix(in srgb, var(--bg-active) 88%, var(--bg-page) 12%);
7441+
border-bottom-color: var(--border-focus);
7442+
}
7443+
74317444
.session-header-left {
74327445
gap: 8px;
74337446
}
@@ -7438,19 +7451,23 @@ textarea.input {
74387451
}
74397452

74407453
.session-title-row {
7454+
display: inline-flex;
7455+
align-items: center;
74417456
gap: 8px;
74427457
min-width: 0;
7458+
flex-wrap: nowrap;
74437459
}
74447460

7445-
.session-title {
7461+
.session-card > .panel-header .panel-header__title {
7462+
font-family: var(--type-code-inline-family);
74467463
font-size: var(--type-code-inline-size);
74477464
line-height: var(--type-code-inline-line-height);
74487465
font-weight: var(--type-code-inline-weight);
74497466
color: var(--text-secondary);
74507467
letter-spacing: 0.04em;
74517468
}
74527469

7453-
.session-card.session-card--active .session-title {
7470+
.session-card.session-card--active > .panel-header .panel-header__title {
74547471
color: var(--text-primary);
74557472
}
74567473

@@ -7476,6 +7493,10 @@ textarea.input {
74767493
white-space: nowrap;
74777494
}
74787495

7496+
.session-card > .panel-header.panel-header--inline-meta .panel-header__meta {
7497+
overflow: hidden;
7498+
}
7499+
74797500
.session-header-actions {
74807501
gap: 4px;
74817502
}
@@ -9262,11 +9283,13 @@ textarea.input {
92629283

92639284
.mobile-shell__agent-stage .session-header-accessory {
92649285
min-width: 0;
9286+
max-width: 100%;
92659287
}
92669288

92679289
.mobile-shell__agent-stage .session-header-right {
9290+
flex: 0 1 auto;
92689291
min-width: 0;
9269-
max-width: min(48%, 220px);
9292+
max-width: 100%;
92709293
}
92719294

92729295
.mobile-shell__agent-stage .session-title-row {

packages/web/src/styles/components.theme.test.ts

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @vitest-environment node
2-
import { readFileSync } from "node:fs";
2+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { join } from "node:path";
34
import { build } from "vite";
45
import { describe, expect, it } from "vitest";
56

@@ -130,38 +131,51 @@ function getLastRuleBlock(selector: string) {
130131
}
131132

132133
async function buildRuntimeStylesheet() {
133-
const output = await build({
134-
configFile: false,
135-
root: process.cwd(),
136-
plugins: [],
137-
build: {
138-
write: false,
139-
outDir: "dist-test",
140-
cssCodeSplit: true,
141-
rollupOptions: {
142-
input: `${process.cwd()}/src/main.tsx`,
143-
},
144-
},
145-
resolve: {
146-
alias: {
147-
"@": `${process.cwd()}/src`,
134+
const tempDir = mkdtempSync(join(process.cwd(), ".vite-style-build-"));
135+
const entryFile = join(tempDir, "runtime-entry.ts");
136+
137+
writeFileSync(
138+
entryFile,
139+
[
140+
'import buttonStyles from "../src/components/ui/button/index.module.css";',
141+
'import tabsStyles from "../src/components/ui/tabs/index.module.css";',
142+
"void [buttonStyles.btn, tabsStyles.tab];",
143+
].join("\n"),
144+
"utf8"
145+
);
146+
147+
try {
148+
const output = await build({
149+
configFile: false,
150+
root: process.cwd(),
151+
plugins: [],
152+
build: {
153+
write: false,
154+
outDir: join(tempDir, "dist"),
155+
cssCodeSplit: true,
156+
lib: {
157+
entry: entryFile,
158+
formats: ["es"],
159+
},
148160
},
149-
},
150-
});
161+
});
151162

152-
const bundle = Array.isArray(output) ? output[0] : output;
153-
const chunks = "output" in bundle ? bundle.output : [];
154-
const cssAssets = chunks.filter(
155-
(chunk): chunk is { type: "asset"; fileName: string; source: string | Uint8Array } =>
156-
chunk.type === "asset" && chunk.fileName.endsWith(".css")
157-
);
163+
const bundle = Array.isArray(output) ? output[0] : output;
164+
const chunks = "output" in bundle ? bundle.output : [];
165+
const cssAssets = chunks.filter(
166+
(chunk): chunk is { type: "asset"; fileName: string; source: string | Uint8Array } =>
167+
chunk.type === "asset" && chunk.fileName.endsWith(".css")
168+
);
158169

159-
expect(cssAssets.length, "expected built CSS asset").toBeGreaterThan(0);
160-
return cssAssets
161-
.map((asset) =>
162-
typeof asset.source === "string" ? asset.source : Buffer.from(asset.source).toString("utf8")
163-
)
164-
.join("\n");
170+
expect(cssAssets.length, "expected built CSS asset").toBeGreaterThan(0);
171+
return cssAssets
172+
.map((asset) =>
173+
typeof asset.source === "string" ? asset.source : Buffer.from(asset.source).toString("utf8")
174+
)
175+
.join("\n");
176+
} finally {
177+
rmSync(tempDir, { recursive: true, force: true });
178+
}
165179
}
166180

167181
describe("components.css theme-sensitive surfaces", () => {
@@ -447,10 +461,10 @@ describe("components.css theme-sensitive surfaces", () => {
447461
const sessionCard = getLastRuleBlock(".session-card");
448462
const activeSessionCard = getLastRuleBlock(".session-card.session-card--active");
449463
const activeSessionHeader = getLastRuleBlock(
450-
".session-card.session-card--active .session-header"
464+
".session-card.session-card--active > .panel-header"
451465
);
452466
const activeSessionTitle = getLastRuleBlock(
453-
".session-card.session-card--active .session-title"
467+
".session-card.session-card--active > .panel-header .panel-header__title"
454468
);
455469
const statusBar = getLastRuleBlock(".workspace-status-bar");
456470
const agentPanes = getLastRuleBlock(".workspace-main-stage > .agent-panes");
@@ -1725,6 +1739,7 @@ describe("components.css theme-sensitive surfaces", () => {
17251739
const progress = getLastRuleBlock(".mobile-shell__agent-stage .session-progress");
17261740
const header = getLastRuleBlock(".mobile-shell__agent-stage > .session-card > .panel-header");
17271741
const titleRow = getLastRuleBlock(".mobile-shell__agent-stage .session-title-row");
1742+
const headerRight = getLastRuleBlock(".mobile-shell__agent-stage .session-header-right");
17281743
const badges = getLastGroupedRuleBlock(
17291744
/\.mobile-shell__agent-stage \.session-provider-badge,\s*\.mobile-shell__agent-stage \.session-state-badge\s*\{([^}]*)\}/g
17301745
);
@@ -1745,12 +1760,15 @@ describe("components.css theme-sensitive surfaces", () => {
17451760
expect(header).toContain("border-bottom:");
17461761
expect(header).not.toContain("linear-gradient(");
17471762
expect(titleRow).toContain("gap: 6px");
1763+
expect(headerRight).toContain("max-width: 100%");
1764+
expect(headerRight).not.toContain("max-width: min(48%, 220px)");
17481765
expect(hasRuleBlock(".mobile-shell__agent-stage .session-header")).toBe(false);
17491766
expect(hasRuleBlock(".mobile-shell__agent-stage .session-header-left")).toBe(false);
17501767
expect(hasRuleBlock(".mobile-shell__agent-stage .session-title")).toBe(false);
17511768
expect(badges).toContain("height: 15px");
17521769
expect(badges).toContain("border-radius: 3px");
17531770
expect(supervisorBadge).toContain("min-height: 26px");
1771+
expect(supervisorBadge).not.toContain("width: max-content");
17541772
expect(supervisorBadge).toContain("border-radius: 4px");
17551773
expect(supervisorBadge).not.toContain("border-radius: var(--radius-lg)");
17561774
expect(supervisorLabel).toContain("font-size: var(--type-kicker-size)");

packages/web/src/ui-preview/catalog.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ describe("UI preview catalog", () => {
122122
const ids = UI_PREVIEW_SCENES.map((scene) => scene.id);
123123
expect(ids).toEqual(
124124
expect.arrayContaining([
125+
"readme-desktop-hero",
126+
"readme-desktop-review",
127+
"readme-mobile-progress",
125128
"workspace-launch-modal",
126129
"command-palette",
127130
"branch-quick-pick",
@@ -308,6 +311,36 @@ describe("UI preview catalog", () => {
308311
expect(screen.getByText(/\+\s+background: var\(--bg-elevated\);/)).toBeInTheDocument();
309312
});
310313

314+
it("renders the README desktop hero scene with a live session and shell terminal", async () => {
315+
renderScene("readme-desktop-hero");
316+
317+
expect(
318+
await screen.findByText("Ship the header polish and verify README visuals")
319+
).toBeInTheDocument();
320+
expect(screen.getByText("Supervisor")).toBeInTheDocument();
321+
expect(screen.getByText("Preview Runner")).toBeInTheDocument();
322+
expect(document.querySelector(".workspace-page--desktop .session-card")).toBeTruthy();
323+
expect(document.querySelector(".workspace-page--desktop .bottom-terminal")).toBeTruthy();
324+
});
325+
326+
it("renders the README desktop review scene with git and diff context", async () => {
327+
renderScene("readme-desktop-review");
328+
329+
expect(await screen.findByText("README capture polish")).toBeInTheDocument();
330+
expect(screen.getByText(/Refine the desktop topbar hierarchy/)).toBeInTheDocument();
331+
expect(document.querySelector(".workspace-page--desktop .git-panel")).toBeTruthy();
332+
expect(document.querySelector(".workspace-page--desktop .workspace-git-editor")).toBeTruthy();
333+
});
334+
335+
it("renders the README mobile progress scene with session continuity and supervisor state", async () => {
336+
renderScene("readme-mobile-progress", "mobile");
337+
338+
expect(await screen.findByText("Resume mobile progress review")).toBeInTheDocument();
339+
expect(screen.getAllByText("Supervisor").length).toBeGreaterThan(0);
340+
expect(document.querySelector(".mobile-shell .session-card")).toBeTruthy();
341+
expect(document.querySelector(".mobile-shell .workspace-status-bar")).toBeTruthy();
342+
});
343+
311344
it("renders the workspace terminal empty review scene", async () => {
312345
renderScene("workspace-terminal-empty-review");
313346

packages/web/src/ui-preview/preview-store.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface UiPreviewCommands {
8888
createdAt: number;
8989
}>
9090
>;
91+
supervisorBySessionId?: Record<string, Supervisor>;
9192
}
9293

9394
export interface UiPreviewSeed {
@@ -370,12 +371,20 @@ function createPreviewDispatcher(seed: UiPreviewSeed): DispatchCommand {
370371
op === "file.delete" ||
371372
op === "supervisor.create" ||
372373
op === "supervisor.update" ||
373-
op === "supervisor.delete" ||
374-
op === "supervisor.get"
374+
op === "supervisor.delete"
375375
) {
376376
return ok({} as unknown as T);
377377
}
378378

379+
if (op === "supervisor.get") {
380+
const sessionId = (args as { sessionId?: string })?.sessionId ?? "";
381+
const supervisor =
382+
commands.supervisorBySessionId?.[sessionId] ??
383+
seed.supervisorBySessionId?.[sessionId] ??
384+
null;
385+
return ok({ supervisor } as unknown as T);
386+
}
387+
379388
return err(`Missing preview handler for ${op}`);
380389
};
381390
}

packages/web/src/ui-preview/scene-metadata.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ describe("ui preview scene metadata", () => {
2020
it("registers the desktop review scene ids", () => {
2121
expect(UI_PREVIEW_SCENE_METADATA.map((scene) => scene.id)).toEqual(
2222
expect.arrayContaining([
23+
"readme-desktop-hero",
24+
"readme-desktop-review",
25+
"readme-mobile-progress",
2326
"workspace-topbar-review",
2427
"workspace-sidebar-files-review",
2528
"workspace-sidebar-git-review",
@@ -62,4 +65,18 @@ describe("ui preview scene metadata", () => {
6265

6366
expect(scene?.capture?.selector).toBe(".settings-mobile-root");
6467
});
68+
69+
it("captures the README scenes from their full workspace shells", () => {
70+
const heroScene = UI_PREVIEW_SCENE_METADATA.find((entry) => entry.id === "readme-desktop-hero");
71+
const reviewScene = UI_PREVIEW_SCENE_METADATA.find(
72+
(entry) => entry.id === "readme-desktop-review"
73+
);
74+
const mobileScene = UI_PREVIEW_SCENE_METADATA.find(
75+
(entry) => entry.id === "readme-mobile-progress"
76+
);
77+
78+
expect(heroScene?.capture?.selector).toBe(".workspace-page");
79+
expect(reviewScene?.capture?.selector).toBe(".workspace-page");
80+
expect(mobileScene?.capture?.selector).toBe("[data-testid='mobile-shell']");
81+
});
6582
});

0 commit comments

Comments
 (0)