From 19c21d57e6ba69ae3cbb4414085e0096be7b5918 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 14 Oct 2026 06:08:23 +0000 Subject: [PATCH 1/2] Fit console tables and tabs on phones A list toolbar no longer widens the page body, so the page never scrolls sideways at 390 px. The Session log shows Status beside each Session, Settings tabs wrap instead of scrolling out of sight, KPI strips use two columns with whole labels, narrow fact cards keep two even columns, and the Request log table scrolls in its frame. --- apps/web/DESIGN.md | 8 ++++---- apps/web/e2e/unified-settings.spec.ts | 10 ++++++++++ apps/web/src/features/overview/overview.css | 1 + .../src/features/request-log/request-log.css | 6 ++++-- .../src/features/sessions/SessionLogPage.tsx | 4 ++-- apps/web/src/features/settings/settings.css | 2 +- apps/web/src/styles/console.css | 18 ++++++++++++++++++ apps/web/src/styles/system.css | 5 +++-- 8 files changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md index b95e7c611..4776ab090 100644 --- a/apps/web/DESIGN.md +++ b/apps/web/DESIGN.md @@ -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. @@ -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. @@ -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. @@ -428,7 +428,7 @@ 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. diff --git a/apps/web/e2e/unified-settings.spec.ts b/apps/web/e2e/unified-settings.spec.ts index 01b3f6bfb..b6a8b095b 100644 --- a/apps/web/e2e/unified-settings.spec.ts +++ b/apps/web/e2e/unified-settings.spec.ts @@ -135,3 +135,13 @@ test("reset progress actions stay inside settings on narrow screens", async ({ p await expect(dialog).toBeHidden(); } }); + +test("a phone keeps the Session log status and every Settings tab on screen", async ({ page, request }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openConsole(page, request, "sessions"); + await expect(page.getByRole("table", { name: "Session log" }).getByRole("columnheader", { name: "Status", exact: true })).toBeInViewport({ ratio: 1 }); + await page.goto("/#settings?id=models"); + 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 }); +}); diff --git a/apps/web/src/features/overview/overview.css b/apps/web/src/features/overview/overview.css index 86b6cafea..8af743313 100644 --- a/apps/web/src/features/overview/overview.css +++ b/apps/web/src/features/overview/overview.css @@ -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; } diff --git a/apps/web/src/features/request-log/request-log.css b/apps/web/src/features/request-log/request-log.css index 89ee1576a..c336523be 100644 --- a/apps/web/src/features/request-log/request-log.css +++ b/apps/web/src/features/request-log/request-log.css @@ -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; } diff --git a/apps/web/src/features/sessions/SessionLogPage.tsx b/apps/web/src/features/sessions/SessionLogPage.tsx index 71416af9d..27da65dd9 100644 --- a/apps/web/src/features/sessions/SessionLogPage.tsx +++ b/apps/web/src/features/sessions/SessionLogPage.tsx @@ -128,8 +128,8 @@ export function SessionLogPage() { {t("log.session")} - {allProjects ? {tCommon("project.column")} : null} {t("log.status")} + {allProjects ? {tCommon("project.column")} : null} {t("log.model")} {t("log.environment")} {t("log.tokens")} @@ -250,8 +250,8 @@ function SessionLogRow({ - {projectCell} event.stopPropagation()}> + {projectCell} {session.agent.model || MISSING} {t(`environment.${environmentKind(session)}`)} diff --git a/apps/web/src/features/settings/settings.css b/apps/web/src/features/settings/settings.css index 8cf3e019d..5129cce3f 100644 --- a/apps/web/src/features/settings/settings.css +++ b/apps/web/src/features/settings/settings.css @@ -182,7 +182,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; } diff --git a/apps/web/src/styles/console.css b/apps/web/src/styles/console.css index 0c7a8663f..bd0d3a328 100644 --- a/apps/web/src/styles/console.css +++ b/apps/web/src/styles/console.css @@ -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; @@ -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, diff --git a/apps/web/src/styles/system.css b/apps/web/src/styles/system.css index b48af71d9..70790f706 100644 --- a/apps/web/src/styles/system.css +++ b/apps/web/src/styles/system.css @@ -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); From 027de42adbee6397070a351f82d8861fddd64877 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 14 Oct 2026 07:36:48 +0000 Subject: [PATCH 2/2] Keep Startup settings columns and page width on phones --- apps/web/DESIGN.md | 2 +- apps/web/e2e/unified-settings.spec.ts | 12 +++++++++--- apps/web/src/features/settings/settings.css | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md index 4776ab090..96bd4058e 100644 --- a/apps/web/DESIGN.md +++ b/apps/web/DESIGN.md @@ -436,7 +436,7 @@ Workspace storage shows the selected configuration's adapter and tenant capacity 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. diff --git a/apps/web/e2e/unified-settings.spec.ts b/apps/web/e2e/unified-settings.spec.ts index b6a8b095b..8b4e17c33 100644 --- a/apps/web/e2e/unified-settings.spec.ts +++ b/apps/web/e2e/unified-settings.spec.ts @@ -136,12 +136,18 @@ test("reset progress actions stay inside settings on narrow screens", async ({ p } }); -test("a phone keeps the Session log status and every Settings tab on screen", async ({ page, request }) => { +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"); - await expect(page.getByRole("table", { name: "Session log" }).getByRole("columnheader", { name: "Status", exact: true })).toBeInViewport({ ratio: 1 }); - await page.goto("/#settings?id=models"); + 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); }); diff --git a/apps/web/src/features/settings/settings.css b/apps/web/src/features/settings/settings.css index 5129cce3f..8f89cc477 100644 --- a/apps/web/src/features/settings/settings.css +++ b/apps/web/src/features/settings/settings.css @@ -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 {