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
133 changes: 69 additions & 64 deletions apps/web/DESIGN.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The console runs beside the administrator's own Core, with execution, files and

## Information Architecture

- **Monitor**: Overview (service status, running Sessions, sandbox slots, Sessions needing attention, 24-hour Session activity, the topology of Core and its nodes with a popover glance at each, the attention table, usage by project), Core metrics (the Core process's CPU and memory, execution slots and the Turn queue, connected daemons, the database and background jobs), Agent metrics (requests, errors, duration, tokens, models, tools, Agents and API keys for 1 h / 6 h / 24 h / 7 d), Sandbox metrics (node capacity and hosted Runtimes across projects; a node or a sandbox opens in a dialog with its figures and CPU and memory charts), Session log (every Session, read-only, with a failed Session's reason under its status, opening one Session's history, which jumps to its failed Turns; a self-hosted Session's page also has its environment's executor credentials). Agent metrics' By Agent table opens an Agent's page and, from its failed Turns, its Sessions in the Session log.
- **Monitor**: Overview (service status, running Sessions, sandbox slots, Sessions needing attention, 24-hour Session activity, a compact inventory of Core and up to four nodes, prioritizing offline and degraded nodes when the list is full, with a popover glance at each, the attention table, usage by project), Core metrics (the Core process's CPU and memory, execution slots and the Turn queue, connected daemons, the database and background jobs), Agent metrics (requests, errors, duration, tokens, models, tools, Agents and API keys for 1 h / 6 h / 24 h / 7 d), Sandbox metrics (node capacity and hosted Runtimes across projects; a node or a sandbox opens in a dialog with its figures and CPU and memory charts), Session log (every Session, read-only, with a failed Session's reason under its status, opening one Session's history, which jumps to its failed Turns; a self-hosted Session's page also has its environment's executor credentials). Agent metrics' By Agent table opens an Agent's page and, from its failed Turns, its Sessions in the Session log.
- **Resources**: Agents, Environment templates, Skills, Files, Vaults. Each list shows one project or all projects, with a Project column when all are shown and a Creator column naming the creating key. Detail pages show the resource's facts and offer Delete.
- **Platform**: Projects and keys (projects, their assets and usage, named keys, write history), Nodes (the node list, capacity, host figures, allocations and individual node operations). Add node asks for limits before issuing its one-time command; installers use Core's public URL and require supported node artifacts. Removal offers the host's uninstall command. System owns installation facts, the Domain and HTTPS secondary page, each harness's default model configuration, startup settings, and a link to the Sandbox configuration secondary page. That page owns setup, resource edits, rollout details and reset. Setup selects a backend, size and Runtime, then asks for a deliberate save; own-machine setup continues to Add node.
- A node whose provider is not ready names the reason (Docker unreachable, no Docker limits, missing Runtime image, no KVM, missing microsandbox components, a host too small) and its fix in the help tip beside its status, wherever that status shows.
Expand Down Expand Up @@ -67,7 +67,7 @@ The console runs beside the administrator's own Core, with execution, files and
## Brand Commitments

- Product name: OpenAgentCore. OpenAgentCore mark assets in `apps/web/public/`.
- Use the OpenAgentCore visual identity: neutral grays and a quiet indigo accent. The console uses Inter and Geist Mono on Beautiful UI's foundation tokens and structure; `DESIGN.md` records the system.
- Use the OpenAgentCore visual identity: neutral grays and a quiet indigo accent. The console uses IBM Plex Sans and Geist Mono, Lucide icons, flat page surfaces and compact resource tables on shared foundation tokens; `DESIGN.md` records the system.

## Evidence on Hand

Expand Down
4 changes: 3 additions & 1 deletion apps/web/e2e/overview-readiness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ test("partial and failed refreshes retain rows with a durable caveat and recover

test("successful empty reads preserve true zero counts and empty states", async ({ page, request }) => {
await openConsole(page, request, "overview", { fresh: true });
await expect(page.locator(".overview-activity .ts-chart-plot").first()).toBeVisible();
await expect(page.locator(".overview-activity")).toContainText("Your first Session starts here");
await expect(page.locator(".overview-activity .ts-chart-plot")).toHaveCount(0);
await expect(page.locator(".getting-started .button.primary")).toHaveCount(1);
await expect(page.getByRole("article").filter({ hasText: "Running Sessions" }).locator(".metric-tile-sub")).toHaveText("0 total, 0 idle");
await expect(failed(page)).toHaveCount(0);
await page.locator(".app-sidebar").getByRole("button", { name: "Session log", exact: true }).click();
Expand Down
10 changes: 5 additions & 5 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"test:acceptance": "playwright test"
},
"dependencies": {
"@oac/agents-client": "workspace:*",
"@base-ui/react": "^1.8.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource/ibm-plex-sans": "^5.3.0",
"@number-flow/react": "^0.6.2",
"@oac/agents-client": "workspace:*",
"@tanstack/react-query": "^5.103.2",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
Expand All @@ -31,16 +31,16 @@
"tailwind-merge": "^3.7.0"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^6.0.4",
"shadow-plugin": "^2.1.1",
"tailwindcss": "^4.3.3",
"typescript": "^5.8.3",
"vite": "^8.1.4",
"vitest": "^4.1.9",
"@types/node": "^26.1.0",
"@playwright/test": "^1.63.0"
"vitest": "^4.1.9"
}
}
9 changes: 5 additions & 4 deletions apps/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 14 additions & 19 deletions apps/web/src/app/beautifui/foundation.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@

:root {
/* surfaces — cool, blue-tinted neutrals */
--page: oklch(0.985 0.001 286.376);
--page: #ffffff;
--canvas: oklch(0.961 0.002 247.84);
--surface: oklch(1 0 0);
--inset: oklch(0.979 0.002 247.839);
--inset: #fafafa;
--hover: oklch(0.97 0.002 247.839);
--hover-2: oklch(0.933 0.003 247.86);

/* ink ramp */
--ink: oklch(0.247 0.006 258.361);
--ink-2: oklch(0.506 0.01 264.477);
--ink-3: oklch(0.695 0.009 264.505);
--ink-3: oklch(0.55 0.009 264.505);

/* borders — solid and crisp, not alpha */
--line: oklch(0.946 0.003 264.542);
--line-strong: oklch(0.912 0.005 258.326);
--line: oklch(0.90 0.003 264.542);
--line-strong: oklch(0.85 0.005 258.326);
--line-soft: oklch(0.966 0.002 264.542);
--grid-line: color-mix(in srgb, var(--line) 78%, transparent);
--field: oklch(0.961 0.001 286.375);
Expand Down Expand Up @@ -77,20 +77,20 @@
}

.dark {
--page: oklch(0.209 0.004 264.477);
--page: #101215;
--canvas: oklch(0.231 0.004 264.487);
--surface: oklch(0.26 0.006 271.191);
--inset: oklch(0.243 0.004 264.492);
--surface: #17191e;
--inset: #14161a;
--hover: oklch(0.289 0.006 271.22);
--hover-2: oklch(0.318 0.007 274.747);

--ink: oklch(0.964 0.002 247.839);
--ink-2: oklch(0.731 0.008 260.731);
--ink-3: oklch(0.541 0.01 264.484);
--ink-3: oklch(0.69 0.01 264.484);

--line: oklch(0.308 0.006 258.354);
--line: #2a2d33;
--line-strong: oklch(0.356 0.007 264.474);
--line-soft: oklch(0.278 0.006 258.354);
--line-soft: #22262c;
--field: oklch(0.293 0.006 271.223);
--stripe: oklch(1 0 0 / 0.055);
--stripe-bg: oklch(0.226 0.004 264.485);
Expand All @@ -109,7 +109,7 @@
--tooltip-bg: oklch(0.182 0.004 264.459);
--tooltip-fg: oklch(0.964 0.002 247.839);
--tooltip-muted: oklch(0.731 0.008 260.731);
--tooltip-border: oklch(0.308 0.006 258.354);
--tooltip-border: #2a2d33;

--shadow-hairline: 0 0 0 1px var(--line);
/* Elevation rings go to a subtle *light* hairline in dark mode: the ring
Expand Down Expand Up @@ -160,14 +160,10 @@
--shadow-overlay: var(--shadow-overlay);

/* type */
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
--font-sans: var(--font-console);
--font-mono: var(--font-mono-face), ui-monospace, "SF Mono", monospace;

/* radii — chips 6, controls 8, cards 10, windows 14, pills 999 */
--radius-chip: 6px;
--radius-control: 8px;
--radius-card: 10px;
--radius-window: 14px;
/* Radii are supplied by the console component tokens in system.css. */

/* easing — strong curves; built-ins are too weak */
--ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
Expand All @@ -185,7 +181,6 @@ html {

body {
color: var(--ink);
font-feature-settings: "cv11", "ss01";
font-size: 14px;
letter-spacing: -0.01em;
line-height: 1.5;
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/components/ConsoleSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function ConsoleSidebar({
<img className="brand-mark" src="/oac-mark.svg" width="18" height="18" alt="" aria-hidden="true" />
</span>
<span className="brand-name">OpenAgentCore</span>
<span className="brand-product">{t("console")}</span>
</div>

<nav className="main-nav console-nav" aria-label={t("mainNavigation")}>
Expand All @@ -85,7 +84,7 @@ export function ConsoleSidebar({
onFocus={() => onIntent?.(view)}
>
{active === view ? <m.span className="nav-active-chip" layoutId="console-nav-active" aria-hidden="true" /> : null}
<Icon size={15} strokeWidth={1.5} aria-hidden="true" />
<Icon size={18} strokeWidth={1.75} aria-hidden="true" />
<span>{label}</span>
</button>
);
Expand All @@ -97,7 +96,7 @@ export function ConsoleSidebar({
<div className="sidebar-footer">
<div className="main-nav sidebar-help">
<button type="button" aria-label={t("showGettingStarted")} title={t("showGettingStarted")} onClick={onGettingStarted}>
<ListChecks size={15} strokeWidth={1.5} aria-hidden="true" />
<ListChecks size={18} strokeWidth={1.75} aria-hidden="true" />
<span>{t("showGettingStarted")}</span>
</button>
</div>
Expand Down
17 changes: 11 additions & 6 deletions apps/web/src/components/console-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function HelpTip({ children, label, id: fixedId }: { children: ReactNode;
onClick={() => { if (position && pinned) hide(); else { show(); setPinned(true); } }}
onKeyDown={(event) => { if (event.key === "Escape") hide(); }}
>
<CircleHelp size={13} strokeWidth={1.7} aria-hidden="true" />
<CircleHelp size={13} strokeWidth={1.75} aria-hidden="true" />
</button>
{/* The explanation stays in the document for assistive technology; the popover is its visual copy. */}
<span id={id} className="visually-hidden">{children}</span>
Expand All @@ -87,19 +87,24 @@ export function HelpTip({ children, label, id: fixedId }: { children: ReactNode;
export function PageHeader({
title,
help,
description,
actions,
headingId,
}: {
title: ReactNode;
help?: ReactNode;
description?: ReactNode;
actions?: ReactNode;
headingId?: string;
}) {
return (
<header className="page-header console-page-header">
<div className="console-page-heading">
<h1 id={headingId}>{title}</h1>
{help ? <HelpTip>{help}</HelpTip> : null}
<div className="console-page-intro">
<div className="console-page-heading">
<h1 id={headingId}>{title}</h1>
{help ? <HelpTip>{help}</HelpTip> : null}
</div>
{description ? <p className="console-page-description">{description}</p> : null}
</div>
{actions ? <div className="page-actions">{actions}</div> : null}
</header>
Expand Down Expand Up @@ -132,7 +137,7 @@ export function RefreshButton({
aria-label={name}
title={!refreshing && updatedAt ? `${name}\n${t("actions.updatedAt", { time: updatedAt })}` : name}
>
<RefreshCw className={refreshing ? "refresh-spinning" : undefined} size={16} strokeWidth={1.6} aria-hidden="true" />
<RefreshCw className={refreshing ? "refresh-spinning" : undefined} size={16} strokeWidth={1.75} aria-hidden="true" />
</button>
);
}
Expand Down Expand Up @@ -279,7 +284,7 @@ export function EmptyState({
}) {
return (
<div className="console-empty">
{Icon ? <Icon size={20} strokeWidth={1.5} aria-hidden="true" /> : null}
{Icon ? <Icon size={24} strokeWidth={1.75} aria-hidden="true" /> : null}
<p className="console-empty-title">{title}{hint ? <HelpTip>{hint}</HelpTip> : null}</p>
{description ? <p className="console-empty-description">{description}</p> : null}
{action ? <div className="console-empty-action">{action}</div> : null}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/list-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function SearchField({
const { t } = useTranslation();
return (
<label className="search-control list-search">
<Search size={14} strokeWidth={1.7} aria-hidden="true" />
<Search size={14} strokeWidth={1.75} aria-hidden="true" />
<input type="search" value={value} placeholder={placeholder} aria-label={label ?? t("list.search")} onChange={(event) => onChange(event.target.value)} />
</label>
);
Expand Down Expand Up @@ -72,7 +72,7 @@ export function CopyIdButton({ id, label }: { id: string; label?: string }) {
const name = copied ? t("actions.copied") : label ?? t("actions.copyId");
return (
<button type="button" className="icon-button ghost copyable-id-button" aria-label={name} title={name} onClick={copy}>
{copied ? <Check size={13} strokeWidth={1.7} aria-hidden="true" /> : <Copy size={13} strokeWidth={1.7} aria-hidden="true" />}
{copied ? <Check size={13} strokeWidth={1.75} aria-hidden="true" /> : <Copy size={13} strokeWidth={1.75} aria-hidden="true" />}
</button>
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/first-run/ConsoleAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function ConsoleAccountMenu() {
const toast = useToast();
if (!account) return null;
return <div className="console-account-menu">
<button type="button" disabled={busy} onClick={async () => {
<button type="button" aria-label={t(busy ? "Signing out…" : "Sign out")} title={t("Sign out")} disabled={busy} onClick={async () => {
setBusy(true);
try { await account.logout(); } catch { setBusy(false); toast.show(t("Could not sign out. Try again."), { tone: "error" }); }
}}><LogOut size={14} aria-hidden="true" /><span>{t(busy ? "Signing out…" : "Sign out")}</span></button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";

import { FleetTopology } from "./FleetTopology";
import { FleetOverview } from "./FleetOverview";
import { node } from "./test-fixtures";

describe("fleet topology popovers", () => {
describe("fleet overview popovers", () => {
it("opens Core and each node in a popover rather than navigating", () => {
const noop = () => undefined;
const html = renderToStaticMarkup(
<FleetTopology
<FleetOverview
nodes={[node("a", { name: "worker-a" }), node("b", { online: false })]}
coreLabel="Running"
coreTone="ok"
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/features/overview/FleetOverview.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";

import { FLEET_LIMIT, overviewNodes } from "./FleetOverview";
import { node } from "./test-fixtures";

describe("fleet overview", () => {
it("shows every node in order when they fit", () => {
const nodes = [node("a"), node("b", { online: false }), node("c")];
expect(overviewNodes(nodes).map((entry) => entry.id)).toEqual(["a", "b", "c"]);
});

it("keeps offline and degraded nodes when not every node fits", () => {
const nodes = Array.from({ length: FLEET_LIMIT + 2 }, (_, index) => node(`n${index}`));
nodes.push(node("down", { online: false }), node("provider", { provider_ready: false }));
const shown = overviewNodes(nodes).map((entry) => entry.id);
expect(shown).toHaveLength(FLEET_LIMIT);
expect(shown.slice(0, 2)).toEqual(["down", "provider"]);
expect(shown.slice(2)).toEqual(["n0", "n1"]);
});
});
Loading
Loading