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
2 changes: 1 addition & 1 deletion apps/ui/.stylelintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module.exports = {
// (`var(--bg)`, `var(--st-working)`, `var(--purple)`, …) reds CI here
// instead of resolving to `inherit`. Anchored on `var(--<name>` 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 `-<suffix>`
// 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]/",
Expand Down
Binary file modified apps/ui/e2e/__screens__/agent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/backlog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge-prs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/narrow-pane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/right-sidebar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/split-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/topic-markdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions apps/ui/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.
Expand Down Expand Up @@ -194,7 +193,6 @@ const App: Component<
<Glyph name="logo" />
</span>
<span class="title">Compass</span>
<span class="subtitle">ADE</span>
</div>

<div class="topbar-sep" />
Expand Down Expand Up @@ -278,8 +276,6 @@ const App: Component<
<RightSidebar />
</Show>

<UsageBar />

<Show when={store.shortcutsOpen()}>
<ShortcutsOverlay />
</Show>
Expand Down
97 changes: 5 additions & 92 deletions apps/ui/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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 ────────────────────────────────────────────────────────────── */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
85 changes: 84 additions & 1 deletion apps/ui/src/components/ChannelView.scroll.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<solidVirtual.VirtualItem["key"], Element> }
| undefined;
let virtualizerSpy: Mock<typeof solidVirtual.createVirtualizer> | 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.
Expand Down Expand Up @@ -112,6 +139,9 @@ beforeEach(() => {
afterEach(() => {
restoreGeometry?.();
restoreGeometry = undefined;
virtualizerSpy?.mockRestore();
virtualizerSpy = undefined;
lastVirtualizer = undefined;
});

function scrollToTop(el: HTMLElement): void {
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion apps/ui/src/components/LogPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions apps/ui/src/components/LogPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ const TracePane: Component = () => {
when={items()}
fallback={
<div class="obs-empty muted">
No session trace — this agent hasn't run yet.
No Session Log yet: this agent hasn't run.
</div>
}
>
{(traceItems) => (
<Show
when={traceItems().length > 0}
fallback={<div class="obs-empty muted">Trace is empty.</div>}
fallback={<div class="obs-empty muted">Session Log is empty.</div>}
>
<div class="obs-trace">
<SessionTrace items={traceItems()} />
Expand Down Expand Up @@ -57,7 +57,7 @@ export const LogPanel: Component<{ agent: Agent }> = (props) => {
return (
<aside
class={["log-panel", { minimized: !store.logOpen() }]}
aria-label="Agent observation log"
aria-label="Session Log"
>
<div class="obs-head">
<Show when={store.logOpen()}>
Expand Down Expand Up @@ -107,7 +107,9 @@ export const LogPanel: Component<{ agent: Agent }> = (props) => {
type="button"
class="obs-min"
aria-label={store.logOpen() ? "Minimize" : "Expand"}
title={store.logOpen() ? "Minimize log panel" : "Expand log panel"}
title={
store.logOpen() ? "Minimize Session Log" : "Expand Session Log"
}
onClick={() => store.toggleLog()}
>
<span
Expand Down
15 changes: 15 additions & 0 deletions apps/ui/src/components/MessageStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ export const MessageStream: Component<{
// undefined item or an index past the new message list. Guard both
// so a mid-change tick renders nothing rather than throwing.
const message = () => (item ? messages()[item.index] : undefined);
// Index-reconciled rows get a new message on a scope switch: drop
// the old key's cache entry so one node never sits under two keys.
let rowEl: HTMLDivElement | undefined;
createEffect(
() => item?.key,
(key, prevKey) => {
if (prevKey === undefined || key === prevKey || !rowEl)
return;
if (virtualizer.elementsCache.get(prevKey) === rowEl) {
virtualizer.elementsCache.delete(prevKey);
}
virtualizer.measureElement(rowEl);
},
);
return (
<Show when={item && message()}>
<div
Expand All @@ -102,6 +116,7 @@ export const MessageStream: Component<{
data-index={item.index}
data-key={item.key}
ref={(el) => {
rowEl = el;
// The measure observer reads data-index off the node, so
// set it before measuring (the ref fires before Solid
// flushes the attribute binding).
Expand Down
Loading
Loading