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
10 changes: 5 additions & 5 deletions apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ A restrained neutral ledger with one indigo voice, three signal colours and a se

A fixed 224px sidebar sits beside the flat main column, separated by a 1px rule. Below 640px it becomes a 56px icon rail with accessible names and no clipped labels. The header is at least 56px tall, with a 20px/600 title and actions on the right; actions wrap onto a new line when the available width cannot contain them. The scrolling body uses `24px 24px 40px` padding and a 24px section gap; horizontal padding becomes 16px below 640px. Wide tables scroll within their own frame.

The recurring shapes in the body are the KPI strip (auto-fit columns, min 158px; three per row below 1180px), chart grids (two equal columns, single below 1180px), full-width resource tables, and a fact row on detail pages. Overview has its own arrangement: Getting started while a step is to do, a four-column metric strip separated by vertical rules, Session activity beside a compact fleet inventory, then flat attention and project usage tables. The activity and fleet grid has a 24px gap and stacks below 680px of content width; the metric strip uses two columns below 560px. Core, E2B and up to four nodes are plain rows that open their pages: Core metrics, Sandbox metrics and the node's page on Nodes. When more nodes exist, offline and degraded nodes take priority and a link leads to the full list. Nodes itself is a plain list with a detail page.
The recurring shapes in the body are the KPI strip (auto-fit columns, min 158px; three per row below 1180px and two below 640px, where labels and units wrap instead of truncating), chart grids (two equal columns, single below 1180px), full-width resource tables, and a fact row on detail pages. Overview has its own arrangement: Getting started while a step is to do, a four-column metric strip separated by vertical rules, Session activity beside a compact fleet inventory, then flat attention and project usage tables. The activity and fleet grid has a 24px gap and stacks below 680px of content width; the metric strip uses two columns below 560px. Core, E2B and up to four nodes are plain rows that open their pages: Core metrics, Sandbox metrics and the node's page on Nodes. When more nodes exist, offline and degraded nodes take priority and a link leads to the full list. Nodes itself is a plain list with a detail page.

Spacing follows a 4px base: 4, 8, 12, 16, 24 (page gutter and section gap). Controls are 32px tall, segmented options 26px, table rows 44px (32px compact), table headers 36px.

Expand Down Expand Up @@ -345,7 +345,7 @@ Selects and inputs are 32px fields filled Well Gray with a Hairline ring inside
### List grammar
Every resource list, the Session log and the project list share one grammar:
- **ListToolbar**: on project-scoped lists the project filter first, then the SearchField (280px, search icon, Paper with the control ring), then any further filters (segmented status or order, selects); the count sits on the right in 12.5px Pencil ("12 total", "3 of 12", "40 loaded" when more exist).
- **Project column**: shown only while All projects is selected, right after the name; archived projects are muted.
- **Project column**: shown only while All projects is selected, right after the name (in the Session log, after the status, so a phone shows each Session's status beside it); archived projects are muted.
- **NameCell**: the first column. The name at 500 weight (a link that turns indigo on hover when the row opens a detail page; a muted fallback such as "Untitled" when the resource has no name) with the compact ID underneath in 11.5px mono. The ID's copy button appears on row hover or focus; the full ID lives in its tooltip.
- **Creator column**: the last column before the actions, headed "Creator" with a help tip. It shows the creating key's name (its prefix when unnamed) with a small "Revoked" flag for revoked keys, "Unknown" in Graphite when Core has no record, and "—" while loading or when the lookup failed.
- **RowActions**: text actions right-aligned at the end of the row, 16px apart, ending with Delete (red on hover). A row click opens the detail page; action clicks do not.
Expand Down Expand Up @@ -391,7 +391,7 @@ A 5px pill rail in Well Gray with an inset hairline and a neutral ink fill. The
Time-series charts live in chart panels (caption 13px/600, legend with series totals, plot) inside one chart-grid card. Lines are 2px round-joined with a surface-ringed end dot; gridlines are crisp Hairlines with 11px tabular ticks; hovering draws a Pencil crosshair, a hover-wash band and a dark tooltip. Missing buckets are gaps, not zeros. Every chart has a 26px table toggle at its top right that opens its numbers in a dialog, so the chart grid keeps its layout. When a range is first shown, bars rise from the baseline in a short left-to-right wave and lines trace from their first point; refreshes of the same range redraw in place.

### Tables
A card with a sticky 36px Paper header in Graphite 12.5px/500 over a Hairline, 44px rows divided by Faint Rules, hover wash, right-aligned tabular numerics, clickable rows where a detail page exists, and the list grammar above. Agent metrics' By Agent table links a saved Agent's name to its page and a nonzero Failed figure (in its red) to the Session log with its project and Agent filters set to that Agent, every status; both turn indigo on hover. The figure counts failed Turns in the range, as the column's help tip says, so the link's name and tooltip give that count and say it opens the Agent's Sessions. A key count in a section heading reads "3 active · 1 revoked" (revoked left out at zero).
A card with a sticky 36px Paper header in Graphite 12.5px/500 over a Hairline, 44px rows divided by Faint Rules, hover wash, right-aligned tabular numerics, clickable rows where a detail page exists, and the list grammar above. A table wider than the screen scrolls inside its frame; the page never scrolls sideways, and toolbar filters wrap above the table on a phone. Agent metrics' By Agent table links a saved Agent's name to its page and a nonzero Failed figure (in its red) to the Session log with its project and Agent filters set to that Agent, every status; both turn indigo on hover. The figure counts failed Turns in the range, as the column's help tip says, so the link's name and tooltip give that count and say it opens the Agent's Sessions. A key count in a section heading reads "3 active · 1 revoked" (revoked left out at zero).

### Notices
On Overview and Session log, failed reads that leave a section unavailable replace its contents with ErrorState and Retry. Partial or stale reads keep useful rows and figures, with a durable ErrorState and Retry beside them explaining that coverage may be incomplete or out of date. A failed read never supplies a zero chart or an all-clear; successfully read zero values stay zero. Session log status counts stay missing until the reads succeed. A failed summary retains its last rows, and a failed project Session read retains only that project's last rows; successful sources update independently. Retention never crosses project scopes.
Expand Down Expand Up @@ -428,15 +428,15 @@ Read deployment progress independently of node details. Partial failures retain
### Settings page
Settings is the single platform configuration entry, with four tabs: Sandbox and scheduling, Workspace storage, Models and credentials, and Service and deployment. Each tab has a stable address; links from Nodes and metrics open the owning tab, and the Sandbox and scheduling header links back to Nodes. Forms save independently through their resource APIs, never through one cross-resource Save all action.

The hierarchy is page, tab, section, row. The page header loses its rule to an underline tab row: 40px text tabs in Graphite 13px/500, the current one Ink with a 2px Ink underline. The body is a left-aligned column at most 880px wide with 32px between sections. A section is the shared Section heading (14px/600, help tip, section-wide outline actions on the right) over one card of label/value rows (`settings.css`): a 200px Graphite label column with its help tip and an Ink value column, 8px by 16px padding, 32px minimum line height, Faint Rules between rows; below 640px the label sits above the value. An action that changes one value sits at the end of its row (Edit tenant capacity; Save beside the idle-seconds field, whose error appears beneath it). Read-only status is a row too (Configuration rollout, Snapshot retention); tables sit in one framed card. Settings never repeats the KPI strips of Nodes or the metrics pages. The irreversible operation closes the page in its own section: a red-ringed card with one Ink line stating the consequence and a red-text outline button.
The hierarchy is page, tab, section, row. The page header loses its rule to an underline tab row: 40px text tabs in Graphite 13px/500, the current one Ink with a 2px Ink underline. Below 640px the tabs wrap onto a second row rather than scroll out of sight. The body is a left-aligned column at most 880px wide with 32px between sections. A section is the shared Section heading (14px/600, help tip, section-wide outline actions on the right) over one card of label/value rows (`settings.css`): a 200px Graphite label column with its help tip and an Ink value column, 8px by 16px padding, 32px minimum line height, Faint Rules between rows; below 640px the label sits above the value. An action that changes one value sits at the end of its row (Edit tenant capacity; Save beside the idle-seconds field, whose error appears beneath it). Read-only status is a row too (Configuration rollout, Snapshot retention); tables sit in one framed card. Settings never repeats the KPI strips of Nodes or the metrics pages. The irreversible operation closes the page in its own section: a red-ringed card with one Ink line stating the consequence and a red-text outline button.

Sandbox and scheduling contains sandbox setup or Deployment provider (Provider, Each sandbox, the E2B connection rows and the rollout row, with Change resources in the section header), then Automatic suspension (the deployment-wide idle policy and read-only snapshot retention, shown only when the deployment declares suspension), then Reset. While a reset runs, its progress section leads the page with Force reset now and Cancel reset in its header. Nodes owns the nodes themselves: status, per-node limits, allocations, host figures and charts, Add node (with its enrollment form), Edit node and removal. Resource changes never imply rewriting existing allocations. Lower limits do not imply forced termination or snapshot deletion.

Workspace storage shows the selected configuration's adapter and tenant capacity (in bytes units, or No tenant capacity limit) under Storage configuration, with Select new configuration in the section header and Edit tenant capacity on the capacity row. The configuration ID and parameters JSON sit in a closed Configuration details disclosure at the end of the card. Select new configuration accepts an adapter identifier, configuration ID, parameters JSON object and optional write-only credential JSON object. Edit tenant capacity keeps the current configuration identity. Explain adapter preparation and existing-workspace behavior behind help tips, with a link to the owning configuration document; keep required form labels and validation visible. Never prefill credentials or persist their drafts across navigation. A failed read is not an unconfigured filesystem, and an uncertain write is not a safe automatic retry.

Models and credentials stacks one default-model card per harness. Its header carries the harness name, its readiness (Ready in green; Model provider not set in amber, with Set as the primary button; Disabled in neutral, with a View startup settings text action), a Default pill, a help tip and the Set, Replace and Clear actions; rows hold model ID, provider protocol, base URL, key presence, token limits when set and update time, and a foot holds Usage details. Set and Replace open one form dialog. The adapter declaration supplies supported protocols, native JSON support and required limits without harness-specific branches. Native settings use an optional JSON object editor with formatting and inline syntax errors. Changing model identity clears native JSON; replacing only the required write-only API key does not. Keys are never prefilled and are forgotten when the form closes. Core rejections remain visible in the form; Clear uses ConfirmDialog. Usage details opens Core's observations separately.

Service and deployment shows installation addresses, an Installation details disclosure, closed by default, with the installation ID and source commit, existing Domain and HTTPS actions, and the startup settings Core loaded. Process settings remain read-only here; their source stays the installation configuration. Each row's label is the setting's environment variable in code style (OAC_LOG_LEVEL), as Core reports it; values show as is, with arrays joined, On or Off for booleans and None for an empty list. How to change a setting in the section header links to the configuration guide. Sensitive settings show only Configured or Not set. Default uses a neutral pill.
Service and deployment shows installation addresses, an Installation details disclosure, closed by default, with the installation ID and source commit, existing Domain and HTTPS actions, and the startup settings Core loaded. Process settings remain read-only here; their source stays the installation configuration. Each row's label is the setting's environment variable in code style (OAC_LOG_LEVEL), as Core reports it, wrapping after an underscore so a phone keeps all three columns; values show as is, with arrays joined, On or Off for booleans and None for an empty list. How to change a setting in the section header links to the configuration guide. Sensitive settings show only Configured or Not set. Default uses a neutral pill.

### One place for each task
Each configuration has exactly one editor in Settings; other pages link to it. Resource operations such as Project keys, Session executor credentials and node removal stay on their resource pages. Keep the configuration page flat: resource editors are dialogs and rollout is one status row with a details action. Explanations belong in question-mark help tips, not small-print helper paragraphs. Errors, uncertain outcomes and current usage exceeding a new limit remain visible without duplicating the workflow.
Expand Down
16 changes: 16 additions & 0 deletions apps/web/e2e/unified-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,19 @@ test("reset progress actions stay inside settings on narrow screens", async ({ p
await expect(dialog).toBeHidden();
}
});

test("a phone scrolls wide tables inside their frames and keeps every Settings tab and column on screen", async ({ page, request }) => {
await page.setViewportSize({ width: 390, height: 844 });
const pageScrollsSideways = () => page.evaluate(() => document.documentElement.scrollWidth > innerWidth);
await openConsole(page, request, "sessions");
const log = page.getByRole("table", { name: "Session log" });
await expect(log.getByRole("columnheader", { name: "Status", exact: true })).toBeInViewport({ ratio: 1 });
expect(await page.locator(".table-frame").filter({ has: log }).evaluate((frame) => { frame.scrollLeft = 100; return frame.scrollLeft > 0; })).toBe(true);
expect(await pageScrollsSideways()).toBe(false);
await page.goto("/#settings?id=service");
const tabs = page.getByRole("navigation", { name: "Settings" }).getByRole("button");
await expect(tabs).toHaveCount(4);
for (const tab of await tabs.all()) await expect(tab).toBeInViewport({ ratio: 1 });
await expect(page.getByRole("table", { name: "Startup settings" }).getByRole("columnheader", { name: "Restarts", exact: true })).toBeInViewport({ ratio: 1 });
expect(await pageScrollsSideways()).toBe(false);
});
1 change: 1 addition & 0 deletions apps/web/src/features/overview/overview.css
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@
.overview-tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 0; }
.metric-tile:nth-child(3) { padding-left: 0; border-left: 0; }
.metric-tile { padding-right: 12px; }
.metric-tile-sub { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
.getting-started, .overview-activity, .overview-fleet, .overview-table-card { animation: none; }
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/features/request-log/request-log.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.request-log-search { display: flex; align-items: center; gap: 8px; }
.request-log-table { table-layout: fixed; }
.request-log-search { display: flex; max-width: 100%; align-items: center; gap: 8px; }
.request-log-search .search-control { min-width: 0; }
/* Fixed columns need room for the request ID; a phone scrolls the table instead. */
.request-log-table { min-width: 640px; table-layout: fixed; }
.request-log-table th:first-child { width: 110px; }
.request-log-table th:nth-child(3) { width: 125px; }
.request-log-table th:last-child { width: 100px; }
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/sessions/SessionLogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export function SessionLogPage() {
<thead>
<tr>
<th scope="col">{t("log.session")}</th>
{allProjects ? <th scope="col">{tCommon("project.column")}</th> : null}
<th scope="col">{t("log.status")}</th>
{allProjects ? <th scope="col">{tCommon("project.column")}</th> : null}
<th scope="col">{t("log.model")}</th>
<th scope="col">{t("log.environment")}</th>
<th scope="col" className="numeric">{t("log.tokens")}</th>
Expand Down Expand Up @@ -250,8 +250,8 @@ function SessionLogRow({
<th scope="row">
<NameCell name={sessionTitle(session)} id={session.id} fallback={tCommon("untitledSession")} onOpen={open} openLabel={t("log.open", { id: session.id })} />
</th>
{projectCell}
<td onClick={(event) => event.stopPropagation()}><SessionStatus projectId={row.project.id} session={session} truncate /></td>
{projectCell}
<td><code>{session.agent.model || MISSING}</code></td>
<td className="session-nowrap">{t(`environment.${environmentKind(session)}`)}</td>
<td className="numeric" title={session.usage ? t("log.exactTokens", { tokens: session.usage.total_tokens.toLocaleString(locale) }) : undefined}>
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/features/settings/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@
}

.settings-table .data-table tbody tr:last-child > * { border-bottom: 0; }
.settings-table .data-table tbody th { font-weight: 500; text-align: left; }
/* Setting names wrap after an underscore, so they need no minimum width and a phone keeps every column. */
.settings-table .data-table tbody th { min-width: 0; font-weight: 500; text-align: left; }

/* The irreversible operation closes the page, apart from the settings above it. */
.settings-danger {
Expand All @@ -182,7 +183,7 @@
.settings-danger .button { flex: none; }

@media (max-width: 640px) {
.settings-tabs { gap: 20px; padding: 0 16px; }
.settings-tabs { flex-wrap: wrap; gap: 0 12px; padding: 0 16px; }
.settings-row { grid-template-columns: minmax(0, 1fr); padding: 8px 12px; }
.settings-row > dt,
.settings-row > .settings-row-label { min-height: 24px; }
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/styles/console.css
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ html[data-theme="dark"] {

.list-toolbar-controls {
display: flex;
min-width: 0;
flex: 1 1 auto;
flex-wrap: wrap;
align-items: center;
Expand Down Expand Up @@ -1215,6 +1216,23 @@ html[data-theme="dark"] {
}
}

/* Phones: two figures per row with whole labels, and a list's filters wrap
inside the page instead of widening it. */
@media (max-width: 640px) {
.kpi-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}

.kpi dt > span:first-child,
.kpi-value {
white-space: normal;
}

.list-toolbar .segmented {
flex-wrap: wrap;
}
}

@media (prefers-reduced-motion: reduce) {
.page-transition,
.console-page-body,
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/styles/system.css
Original file line number Diff line number Diff line change
Expand Up @@ -1084,10 +1084,11 @@ pre {

/* Detail pages ---------------------------------------------------------- */

/* A resource's facts sit in one card on an even grid, labels above values. */
/* A resource's facts sit in one card on an even grid, labels above values.
A narrow card keeps two even columns, so a two-column ID fact still fits. */
.resource-facts,
.template-facts {
grid-template-columns: repeat(auto-fill, minmax(176px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(min(176px, calc(50% - 12px)), 1fr));
gap: 14px 24px;
padding: 14px 16px 15px;
background: var(--surface);
Expand Down
Loading