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
9 changes: 9 additions & 0 deletions packages/app-web/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ link beside it has the rest:
| A file | `commit:changes` | the commits that changed it | `history` |
| An act | `ran_against:commit` | its `git_hash`, when the workspace's repository holds it | — |

## Pages and their chrome

Every page of the app, the Trace Console and the development pages (the gallery, the sessions list,
session replays and the agent), shares one frame: `src/ui/chrome/Frame.tsx` and `chrome.css`. The
frame gives a page the palette, the top bar and the resizable border. The theme is one setting
(`useSavedTheme`) that every component reads, so the button in the top bar and the content agree.
The development pages have no link in the Trace Console, except `agent`, and are reached by their
addresses.

## Trace Console

The browser app's page for the graph, at `/app/<API path>`: `/app/workspace/overlap_bench/Q_1` opens
Expand Down
25 changes: 25 additions & 0 deletions packages/app-web/e2e/app.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ test.describe("getting around", () => {
await expect(bar).not.toContainText("live");
});

test("the top bar links to no gallery or sessions page", async ({ page }) => {
await page.goto("/app/workspace/alpha/Q_1");
const bar = page.locator(".topbar");
await expect(bar.getByRole("button", { name: "LabKit" })).toBeVisible();
await expect(bar.getByRole("link", { name: "gallery" })).toHaveCount(0);
await expect(bar.getByRole("link", { name: "sessions" })).toHaveCount(0);
});

test("the sessions page has the same top bar as the Trace Console, with the theme button at its right", async ({
page,
}, info) => {
test.skip(info.project.name !== "dev", "the sessions page exists only in development");
await page.goto("/app/gallery/sessions");
const bar = page.locator(".topbar");
await expect(bar.getByRole("link", { name: "LabKit" })).toBeVisible();
const theme = bar.getByRole("button", { name: /^Theme:/ });
await expect(theme).toBeVisible();
const barBox = await bar.boundingBox();
const themeBox = await theme.boundingBox();
expect(
(barBox?.x ?? 0) + (barBox?.width ?? 0) - ((themeBox?.x ?? 0) + (themeBox?.width ?? 0)),
).toBeLessThan(24);
expect(await bar.evaluate((e) => getComputedStyle(e).height)).toBe("52px");
});

test("the Items filter narrows the listed items", async ({ page }) => {
await page.goto(`/app/workspace/alpha/Q_1?${list("/workspace/alpha/question")}`);
await expect(item(page, "Q_2")).toBeVisible();
Expand Down
7 changes: 3 additions & 4 deletions packages/app-web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { Bar } from "../ui/Bar";
import { Frame } from "../ui/chrome/Frame";

export const Route = createRootRoute({
component: Outlet,
notFoundComponent: () => (
<>
<Bar />
<Frame>
<div className="page">
<h2>Not found</h2>
<p>
Expand All @@ -14,6 +13,6 @@ export const Route = createRootRoute({
</Link>
</p>
</div>
</>
</Frame>
),
});
189 changes: 4 additions & 185 deletions packages/app-web/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
/* The page's own palette lives in the chrome (`src/ui/chrome/chrome.css`); this is the page. */
:root {
--bg: #0b0e14;
--panel: #12151d;
--panel-border: #232838;
--text: #d7dce4;
--text-dim: #808a9c;
--accent: #5ad1c9;
--accent-dim: #2c4a48;
--amber: #e0b25a;
--danger: #e0687a;
--font-sans: "IBM Plex Sans";
--font-mono: "IBM Plex Mono", "SF Mono", Menlo, Consolas, monospace;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
Expand All @@ -19,7 +9,7 @@
box-sizing: border-box;
/* A scrollbar is a thin thumb and nothing else: no track, no border, no background. */
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text-dim) 45%, transparent) transparent;
scrollbar-color: color-mix(in srgb, var(--lk-muted) 45%, transparent) transparent;
}

html,
Expand All @@ -28,8 +18,8 @@ body,
margin: 0;
height: 100%;
width: 100%;
background: var(--bg);
color: var(--text);
background: var(--lk-bg);
color: var(--lk-text);
overflow: hidden;
}

Expand All @@ -40,86 +30,6 @@ body,
height: 100vh;
}

#bar {
display: flex;
align-items: center;
gap: 20px;
padding: 10px 16px;
background: var(--panel);
border-bottom: 1px solid var(--panel-border);
flex: 0 0 auto;
}

#bar h1 {
font-size: 15px;
font-weight: 600;
margin: 0;
letter-spacing: 0.02em;
white-space: nowrap;
}
#bar h1 span {
color: var(--accent);
font-weight: 400;
}

.field {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-dim);
}
/* biome-ignore lint/style/noDescendingSpecificity: source order is the cascade the explorer was designed against */
.field span {
text-transform: uppercase;
letter-spacing: 0.05em;
}

/* The Trace Console (`.trace`) styles its own controls. */
select:where(:not(.trace *)),
button:where(:not(.trace *)),
input[type="range"]:where(:not(.trace *)) {
font-family: inherit;
font-size: 12px;
color: var(--text);
background: #191d28;
border: 1px solid var(--panel-border);
border-radius: 4px;
}

select {
padding: 5px 8px;
min-width: 220px;
}

button:where(:not(.trace *)) {
padding: 5px 10px;
cursor: pointer;
}
button:where(:not(.trace *)):hover {
border-color: var(--accent);
color: var(--accent);
}
button:where(:not(.trace *)):active {
transform: translateY(1px);
}
/* The theme button: an icon alone, as tall as the text buttons beside it. */
button.theme-toggle {
display: inline-flex;
align-items: center;
padding: 4px 8px;
}

.view-toggle {
display: flex;
gap: 2px;
}
.view-toggle button.active {
background: var(--accent-dim);
border-color: var(--accent);
color: var(--accent);
}

main {
flex: 1 1 auto;
display: flex;
Expand All @@ -137,94 +47,3 @@ main {
width: 100%;
height: 100%;
}

/* Pages other than the graph: workspaces, collections. */
.crumb {
color: var(--text-dim);
font-size: 13px;
text-decoration: none;
}
.crumb:hover {
color: var(--accent);
}

.page {
flex: 1 1 auto;
overflow: auto;
padding: 20px 24px;
}
.page h2 {
margin: 0 0 16px;
font-size: 16px;
font-weight: 600;
}
.page a {
color: var(--accent);
text-decoration: none;
}
.page a:hover {
text-decoration: underline;
}

.dim {
color: var(--text-dim);
}

.cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.cards li {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 180px;
padding: 12px 14px;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 6px;
}

.items {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.items td {
padding: 6px 10px 6px 0;
border-bottom: 1px solid var(--panel-border);
vertical-align: top;
}

.paging {
display: flex;
gap: 16px;
margin-top: 16px;
font-size: 12px;
}

/* After the page rules: the same element, and this one is the more specific. */
#bar h1 a {
color: inherit;
text-decoration: none;
}

/* A page that failed to load: the error the API or the router gave. */
.load-error {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 3px 8px;
border-radius: 3px;
color: var(--danger);
border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
background: color-mix(in srgb, var(--danger) 12%, transparent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 42ch;
}
7 changes: 3 additions & 4 deletions packages/app-web/src/ui/AgentSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { initialState, reduce } from "@labkit/view-model";
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { AGENT_CWD, AGENT_URL } from "./agent-endpoint";
import { Bar } from "./Bar";
import { Frame } from "./chrome/Frame";
import { useSavedTheme } from "./saved-theme";

type Status = "connecting" | "ready" | "failed";
Expand Down Expand Up @@ -177,8 +177,7 @@ export default function AgentSession({ sessionId }: { sessionId?: string }) {
void navigate({ to: "/agent/{-$sessionId}", params: { sessionId: started }, replace: true });
}, [started, navigate]);
return (
<>
<Bar />
<Frame>
<div style={{ padding: "8px 16px", color: "var(--text-dim)", fontSize: 12 }}>
{real ? (
`Agent (${status})`
Expand Down Expand Up @@ -220,6 +219,6 @@ export default function AgentSession({ sessionId }: { sessionId?: string }) {
resolveLink={resolveLink}
/>
</div>
</>
</Frame>
);
}
36 changes: 0 additions & 36 deletions packages/app-web/src/ui/Bar.tsx

This file was deleted.

15 changes: 5 additions & 10 deletions packages/app-web/src/ui/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { FIXTURES } from "@labkit/acp-scenarios";
import { Conversation, ThemeToggle } from "@labkit/ui";
import { Conversation } from "@labkit/ui";
import { RECORD_TYPES } from "./record-types";
import "@labkit/ui/ui.css";
import type { TranscriptState } from "@labkit/view-model";
import { stateOfFixture } from "@labkit/view-model/fixtures";
import { Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { Bar } from "./Bar";
import { Frame } from "./chrome/Frame";
import { BuildingBlocks } from "./BuildingBlocks";
import { useSavedTheme } from "./saved-theme";
import { type TranscriptEntry, transcriptList, transcriptState } from "./transcripts-api";
Expand All @@ -22,7 +22,7 @@ import { type TranscriptEntry, transcriptList, transcriptState } from "./transcr
export default function Gallery() {
const [entries, setEntries] = useState<readonly TranscriptEntry[]>(FIXTURES);
const [states, setStates] = useState<Record<string, TranscriptState>>({});
const [theme, setTheme] = useSavedTheme();
const [theme] = useSavedTheme();
const [recorded, setRecorded] = useState<ReadonlySet<string>>(new Set());
// Why a card, or the list of recorded transcripts, could not be loaded, by id ("" for the list).
const [failures, setFailures] = useState<Readonly<Record<string, string>>>({});
Expand Down Expand Up @@ -56,12 +56,7 @@ export default function Gallery() {
}, []);

return (
<>
<Bar>
<span style={{ marginLeft: "auto", display: "flex", gap: 6 }}>
<ThemeToggle theme={theme} onChange={setTheme} className="theme-toggle" />
</span>
</Bar>
<Frame>
<div
style={{
flex: 1,
Expand Down Expand Up @@ -109,6 +104,6 @@ export default function Gallery() {
);
})}
</div>
</>
</Frame>
);
}
Loading
Loading