diff --git a/apps/ui/.stylelintrc.cjs b/apps/ui/.stylelintrc.cjs index 15c1f52ff..65e60b7be 100644 --- a/apps/ui/.stylelintrc.cjs +++ b/apps/ui/.stylelintrc.cjs @@ -38,7 +38,7 @@ module.exports = { // (`var(--bg)`, `var(--st-working)`, `var(--purple)`, …) reds CI here // instead of resolving to `inherit`. Anchored on `var(--` with a // name-boundary char `[-),\s]`, so `var(--cx-bg…)` / `var(--cx-st-*)` - // (the DS tier) and the layout knobs `--topbar-h`/`--usage-h`/`--right-w` + // (the DS tier) and the layout knobs `--topbar-h`/`--right-w` // are NOT matched — only the exact retired names and their `-` // families (`--bg-raised`, `--text-dim`, `--border-strong`, …). "/var\\(\\s*--(bg|text|st|accent|purple|pink|radius|font-mono|add|del|warn|border|danger|surface|fg|bg-inset|text-muted)[-),\\s]/", diff --git a/apps/ui/e2e/__screens__/agent.png b/apps/ui/e2e/__screens__/agent.png index 0d04e32f3..a14d4dfe8 100644 Binary files a/apps/ui/e2e/__screens__/agent.png and b/apps/ui/e2e/__screens__/agent.png differ diff --git a/apps/ui/e2e/__screens__/backlog.png b/apps/ui/e2e/__screens__/backlog.png index 1790d7bfb..d257603e3 100644 Binary files a/apps/ui/e2e/__screens__/backlog.png and b/apps/ui/e2e/__screens__/backlog.png differ diff --git a/apps/ui/e2e/__screens__/bridge-empty.png b/apps/ui/e2e/__screens__/bridge-empty.png index 3bb2cbb7c..086a0ec0f 100644 Binary files a/apps/ui/e2e/__screens__/bridge-empty.png and b/apps/ui/e2e/__screens__/bridge-empty.png differ diff --git a/apps/ui/e2e/__screens__/bridge-prs.png b/apps/ui/e2e/__screens__/bridge-prs.png index 5490feded..20696f7d0 100644 Binary files a/apps/ui/e2e/__screens__/bridge-prs.png and b/apps/ui/e2e/__screens__/bridge-prs.png differ diff --git a/apps/ui/e2e/__screens__/bridge.png b/apps/ui/e2e/__screens__/bridge.png index d5251bdf2..1e4eeebb1 100644 Binary files a/apps/ui/e2e/__screens__/bridge.png and b/apps/ui/e2e/__screens__/bridge.png differ diff --git a/apps/ui/e2e/__screens__/done.png b/apps/ui/e2e/__screens__/done.png index 2e0c8131b..55c2822c7 100644 Binary files a/apps/ui/e2e/__screens__/done.png and b/apps/ui/e2e/__screens__/done.png differ diff --git a/apps/ui/e2e/__screens__/narrow-pane.png b/apps/ui/e2e/__screens__/narrow-pane.png index 15046d519..2f207a1bd 100644 Binary files a/apps/ui/e2e/__screens__/narrow-pane.png and b/apps/ui/e2e/__screens__/narrow-pane.png differ diff --git a/apps/ui/e2e/__screens__/right-sidebar.png b/apps/ui/e2e/__screens__/right-sidebar.png index 01ff4f68f..cfda6d930 100644 Binary files a/apps/ui/e2e/__screens__/right-sidebar.png and b/apps/ui/e2e/__screens__/right-sidebar.png differ diff --git a/apps/ui/e2e/__screens__/settings.png b/apps/ui/e2e/__screens__/settings.png index 9539eef80..403124796 100644 Binary files a/apps/ui/e2e/__screens__/settings.png and b/apps/ui/e2e/__screens__/settings.png differ diff --git a/apps/ui/e2e/__screens__/split-view.png b/apps/ui/e2e/__screens__/split-view.png index 97ae9982d..723825e2e 100644 Binary files a/apps/ui/e2e/__screens__/split-view.png and b/apps/ui/e2e/__screens__/split-view.png differ diff --git a/apps/ui/e2e/__screens__/topic-markdown.png b/apps/ui/e2e/__screens__/topic-markdown.png index 3275df2bc..e4b28922b 100644 Binary files a/apps/ui/e2e/__screens__/topic-markdown.png and b/apps/ui/e2e/__screens__/topic-markdown.png differ diff --git a/apps/ui/src/App.test.tsx b/apps/ui/src/App.test.tsx index c54ac3a06..80e5827e7 100644 --- a/apps/ui/src/App.test.tsx +++ b/apps/ui/src/App.test.tsx @@ -192,6 +192,15 @@ describe("App shell (T7)", () => { flushSync(); expect(leftPresent()).toBe(true); }); + + // The top bar shows the bare wordmark and the shell has no bottom usage bar. + test("the shell carries no ADE subtitle and no usage bar", () => { + const { container } = mountApp(); + expect(container.querySelector(".topbar .brand")?.textContent).toBe( + "Compass", + ); + expect(container.querySelector("footer")).toBeNull(); + }); }); // Coaching-tooltip adoption sweep (RIG-2530 T2). The two glyph-only sidebar diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 0a7500f27..c95549229 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -30,7 +30,6 @@ import { ShortcutsOverlay } from "./components/ShortcutsOverlay"; import { SplitPane } from "./components/SplitPane"; import { TabStrip } from "./components/TabStrip"; import { TopBarSearch } from "./components/TopBarSearch"; -import { UsageBar } from "./components/UsageBar"; import { useStore } from "./context"; import type { CommandId } from "./keyboard/commands"; import { detectPlatform, installKeymap } from "./keyboard/dispatch"; @@ -40,7 +39,7 @@ import { routeTitle } from "./route-title"; import { focusViewPanel } from "./view-panel"; import { focusedPane, focusedViewOf, shownViewIds } from "./window-layout"; -// Compass shell: routed center view with persistent navigation and usage chrome. +// Compass shell: routed center view with persistent navigation chrome. // App owns the shell layout; the matched route renders in its center region. // Bind the store router seam to the active router location and navigation. @@ -194,7 +193,6 @@ const App: Component< Compass - ADE
@@ -278,8 +276,6 @@ const App: Component< - - diff --git a/apps/ui/src/app.css b/apps/ui/src/app.css index b7350fadb..fd484629d 100644 --- a/apps/ui/src/app.css +++ b/apps/ui/src/app.css @@ -4,7 +4,6 @@ counterpart; kept here and allowlisted by name in the D7 stylelint guard. */ :root { --topbar-h: 44px; - --usage-h: 26px; /* Right sidebar width (dock-in-sidebar D4). The single knob the deferred * resize task (D7/T4) will drive; 400px is the locked default, comfortable * for the conversation text now that the fleet chats live here. */ @@ -17,11 +16,10 @@ display: grid; height: 100%; grid-template-columns: auto 1fr auto; - grid-template-rows: var(--topbar-h) 1fr var(--usage-h); + grid-template-rows: var(--topbar-h) 1fr; grid-template-areas: "topbar topbar topbar" - "left main right" - "usage usage usage"; + "left main right"; } /* ── Top bar ────────────────────────────────────────────────────────────── */ @@ -54,13 +52,6 @@ letter-spacing: 0.01em; } -.brand .subtitle { - color: var(--cx-text-dim); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; -} - .topbar-sep { width: 1px; align-self: stretch; @@ -1347,7 +1338,7 @@ grid-area: right; width: var(--right-w); background: var(--cx-bg-raised); - border-left: 1px solid var(--cx-border); + border-left: 1px solid var(--cx-border-strong); display: flex; flex-direction: column; overflow: hidden; @@ -1573,84 +1564,6 @@ background: var(--cx-ok); } -/* ── Usage bar (bottom status strip) ────────────────────────────────────── */ - -.usage { - grid-area: usage; - display: flex; - align-items: center; - gap: 16px; - padding: 0 12px; - background: var(--cx-bg-panel); - border-top: 1px solid var(--cx-border); - font-size: 11px; -} - -.usage-item { - display: flex; - align-items: center; - gap: 8px; -} - -.usage-item .u-provider { - font-weight: 600; - color: var(--cx-text); -} - -.usage-item .u-plan { - color: var(--cx-text-faint); - font-size: 10px; -} - -.usage-meter { - width: 90px; - height: 5px; - border-radius: 3px; - background: var(--cx-bg-panel); - overflow: hidden; -} - -.usage-meter .fill { - height: 100%; - background: var(--cx-accent); -} - -.usage-meter .fill.hot { - background: var(--cx-warn); -} - -.usage-item .u-pct { - color: var(--cx-text-dim); - font-family: var(--cx-font-ui); - font-variant-numeric: tabular-nums; -} - -.usage-item .u-reset { - color: var(--cx-text-faint); - font-family: var(--cx-font-ui); -} - -.usage-spacer { - flex: 1; -} - -.usage-git { - display: flex; - align-items: center; - gap: 6px; - color: var(--cx-text-dim); - font-family: var(--cx-font-ui); -} - -/* Seat the inline branch glyph (state-dot.css §seating). */ -.usage-git-glyph { - display: inline-flex; -} - -.usage-git-glyph > svg { - display: block; -} - /* ── Shared helpers ─────────────────────────────────────────────────────── */ .muted { @@ -3493,7 +3406,7 @@ flex-direction: column; min-height: 0; width: 340px; - border-left: 1px solid var(--cx-border); + border-left: 1px solid var(--cx-border-strong); background: var(--cx-bg-raised); grid-area: log; } @@ -3653,7 +3566,7 @@ .log-panel.minimized { width: auto; border-left: none; - border-top: 1px solid var(--cx-border); + border-top: 1px solid var(--cx-border-strong); } .log-panel.minimized .obs-head { diff --git a/apps/ui/src/components/ChannelView.scroll.test.tsx b/apps/ui/src/components/ChannelView.scroll.test.tsx index 6855400a3..0be6787ab 100644 --- a/apps/ui/src/components/ChannelView.scroll.test.tsx +++ b/apps/ui/src/components/ChannelView.scroll.test.tsx @@ -1,4 +1,13 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + afterEach, + beforeEach, + describe, + expect, + type Mock, + spyOn, + test, +} from "bun:test"; +import * as solidVirtual from "@rigelbuild/solid-virtual"; import { render } from "@solidjs/testing-library"; import { createSignal, flush } from "solid-js"; import type { Account, Message } from "../comms-stub"; @@ -13,6 +22,24 @@ import { } from "./conv-virtual"; import { MessageStream } from "./MessageStream"; +// MessageStream keeps its virtualizer private; a pass-through spy on the real +// factory (behavior unchanged) exposes the measurement cache for inspection. +let lastVirtualizer: + | { elementsCache: Map } + | undefined; +let virtualizerSpy: Mock | undefined; + +function captureVirtualizer(): void { + const real = solidVirtual.createVirtualizer; + virtualizerSpy = spyOn(solidVirtual, "createVirtualizer").mockImplementation( + (options) => { + const instance = real(options); + lastVirtualizer = instance; + return instance; + }, + ); +} + // The conversation stream's scroll contract, designed fresh // (ChannelView had NO scroll management before this lane). The behavior is // TanStack Virtual's chat mode; these tests pin the CONTRACT, not pixel math. @@ -112,6 +139,9 @@ beforeEach(() => { afterEach(() => { restoreGeometry?.(); restoreGeometry = undefined; + virtualizerSpy?.mockRestore(); + virtualizerSpy = undefined; + lastVirtualizer = undefined; }); function scrollToTop(el: HTMLElement): void { @@ -367,6 +397,59 @@ describe("MessageStream scroll contract", () => { ).toBe(199); // B's final message is in view }); + // A topic switch reuses the rendered rows by index; a reused row now shows a + // different message, so it must be measured again or it keeps the estimate. + test("switching scope re-measures reused rows so message spacing stays uniform", () => { + const proto = Object.getPrototypeOf(document.createElement("div")); + const geometry = Object.getOwnPropertyDescriptor(proto, "offsetHeight"); + Object.defineProperty(proto, "offsetHeight", { + configurable: true, + get(this: HTMLElement) { + if (!this.hasAttribute?.("data-index")) + return geometry?.get?.call(this); + return this.textContent?.includes("tall") ? 160 : 40; + }, + }); + const { setMessages, setScopeId, rows } = mountStream([ + msg("a-0", 1_000, "short a"), + msg("a-1", 2_000, "short b"), + ]); + setMessages([ + msg("b-0", 3_000, "short c"), + msg("b-1", 4_000, "tall d"), + msg("b-2", 5_000, "short e"), + ]); + setScopeId("top-b"); + flush(); + expect(rows().map(translateY)).toEqual([0, 40, 200]); + }); + + // A reused row must leave its previous key's cache entry; otherwise one node + // piles up under every key it has shown and is pinned after it detaches. + test("a reused row is cached only under the key it currently shows", () => { + captureVirtualizer(); + const { setMessages, setScopeId, rows } = mountStream([ + msg("a-0", 1_000), + msg("a-1", 2_000), + ]); + for (const scope of ["b", "c", "d"]) { + setMessages([msg(`${scope}-0`, 3_000), msg(`${scope}-1`, 4_000)]); + setScopeId(`top-${scope}`); + flush(); + } + const cache = lastVirtualizer?.elementsCache; + expect(cache).toBeDefined(); + const live = rows(); + expect(live.map((r) => r.getAttribute("data-key"))).toEqual(["d-0", "d-1"]); + for (const row of live) { + expect(cache?.get(row.getAttribute("data-key") ?? "")).toBe(row); + } + for (const [key, el] of cache ?? []) { + const shown = live.find((row) => row === el); + if (shown) expect(key).toBe(shown.getAttribute("data-key") ?? ""); + } + }); + // Case (8): shrinking a large WINDOWED topic to empty ([]) renders the empty // state instead of throwing. REGRESSION for the HIGH crash finding: the // virtualizer core invokes getItemKey/estimateSize INTERNALLY with indices diff --git a/apps/ui/src/components/LogPanel.test.tsx b/apps/ui/src/components/LogPanel.test.tsx index 033969d29..9b41973b0 100644 --- a/apps/ui/src/components/LogPanel.test.tsx +++ b/apps/ui/src/components/LogPanel.test.tsx @@ -123,8 +123,13 @@ describe("LogPanel (T-U2)", () => { test("empty state for an agent without a session", () => { const { container } = mountLogPanel("acc-compass-comms"); - expect(container.querySelector(".obs-empty")).not.toBeNull(); + expect(container.querySelector(".obs-empty")?.textContent).toContain( + "Session Log", + ); expect(container.querySelector(".obs-trace")).toBeNull(); + expect( + container.querySelector("aside.log-panel")?.getAttribute("aria-label"), + ).toBe("Session Log"); }); // SHELL INVARIANT (stays GREEN): minimizing collapses the panel — the trace diff --git a/apps/ui/src/components/LogPanel.tsx b/apps/ui/src/components/LogPanel.tsx index 2af47d23e..80b126d30 100644 --- a/apps/ui/src/components/LogPanel.tsx +++ b/apps/ui/src/components/LogPanel.tsx @@ -20,14 +20,14 @@ const TracePane: Component = () => { when={items()} fallback={
- No session trace — this agent hasn't run yet. + No Session Log yet: this agent hasn't run.
} > {(traceItems) => ( 0} - fallback={
Trace is empty.
} + fallback={
Session Log is empty.
} >
@@ -57,7 +57,7 @@ export const LogPanel: Component<{ agent: Agent }> = (props) => { return (