From d4eff888d74e0e86cfb2c9f7ee199a16f2b539a5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 02:50:55 +0000 Subject: [PATCH 1/6] fix(explorer): stack wrapped lanes, align lane headers, add hover halo Co-authored-by: Zack Jackson --- dashboard/src/workspaces/explorer/Lane.tsx | 50 +++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/dashboard/src/workspaces/explorer/Lane.tsx b/dashboard/src/workspaces/explorer/Lane.tsx index ca1fa061b8..598bee0c85 100644 --- a/dashboard/src/workspaces/explorer/Lane.tsx +++ b/dashboard/src/workspaces/explorer/Lane.tsx @@ -69,12 +69,21 @@ export function Lane({ className="relative flex min-h-[var(--pane-min-height)] min-w-0 flex-col border border-edge-subtle bg-surface-1" > -
- -

{spec.label}

- - - + {/* Name over state, so four headers align whatever length a state's + * detail runs to. The chip truncates its detail; the body repeats the + * full sentence for any lane that has no rows to show instead. */} +
+ + +

{spec.label}

+ + + +
{/* The headline count, or the absence of one. A lane that did not @@ -133,7 +142,15 @@ export function Lane({ {answered ? `${rows.length.toLocaleString()} shown` : '—'} {answered && read.hasMore === true ? ' · more rows remain past this page' : ''}
- {answered ? 'source order · no cross-lane rank' : searching ? 'not served' : 'no rows'} + + {answered + ? 'source order · no cross-lane rank' + : read.state === 'pending' + ? 'reading' + : searching + ? 'not served' + : 'no rows'} + ); @@ -343,9 +360,11 @@ function LaneRow({ className={cn( 'relative flex w-full flex-col justify-center gap-0.5 border-b border-edge-subtle pl-3.5 pr-2.5 text-left', 'transition-[opacity,background-color,box-shadow] duration-[var(--dur-state)] ease-[var(--ease-standard)] motion-reduce:transition-none', - // Hover raises the face one plane; selection is the gutter, not a wash, - // so the two never read as the same thing. - 'hover:bg-raised hover:shadow-[var(--shadow-raised)] focus-visible:bg-surface-2', + // Hover raises the face one plane and draws one restrained halo; + // selection is the gutter, not a wash, so the two never read as the + // same thing. Keyboard focus keeps the shell's 2px outline. + 'hover:bg-raised hover:shadow-[var(--shadow-raised)] hover:outline hover:outline-1 hover:-outline-offset-1 hover:outline-accent/40', + 'focus-visible:bg-surface-2 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent', selected && 'bg-surface-2', )} > @@ -468,7 +487,16 @@ export function LaneGrid({ children }: { children: ReactNode }) { role="group" aria-label="Result lanes" onKeyDown={onKeyDown} - className="td-stagger grid min-h-0 flex-1 auto-rows-fr gap-2 p-2 [grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))]" + className={cn( + 'td-stagger grid gap-2 p-2 [grid-template-columns:repeat(auto-fit,minmax(15rem,1fr))]', + // One row at `lg` and above: the grid takes the aperture's height and + // each lane scrolls its own rows. Where the lanes wrap — narrow + // viewports, 200% zoom — every row is a fixed lane height instead of + // a share of a height too small to hold them, so wrapped lanes stack + // and the page scrolls rather than the rows overlapping. + 'lg:min-h-0 lg:flex-1 lg:auto-rows-fr', + 'max-lg:flex-none max-lg:auto-rows-[28rem]', + )} > {children} From f2c4c36328930716048febb4c27377ae10c101db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 02:57:04 +0000 Subject: [PATCH 2/6] feat(explorer): surface source freshness and count evidence per lane Co-authored-by: Zack Jackson --- .../explorer/ExplorerPage.dom.test.tsx | 3 +- dashboard/src/workspaces/explorer/Lane.tsx | 38 ++++++++++++++----- .../src/workspaces/explorer/evidence.test.ts | 2 + .../src/workspaces/explorer/laneModel.test.ts | 13 +++++++ .../src/workspaces/explorer/laneModel.ts | 17 +++++++++ 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx b/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx index c9ea4b7842..00535c1b98 100644 --- a/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx +++ b/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx @@ -329,9 +329,10 @@ describe('ExplorerPage independent lane lifecycle', () => { expect(knowledge.queryByText('0')).toBeNull(); expect(knowledge.getByText('UNAVAILABLE')).toBeTruthy(); - // The ready lanes carry their own counts and grade, untouched. + // The ready lanes carry their own counts, grade and freshness, untouched. expect(within(lane('Code')).getByText('1')).toBeTruthy(); expect(within(lane('Code')).getByText('EXACT')).toBeTruthy(); + expect(lane('Code').querySelector('[data-lane-freshness="unknown"]')).toBeTruthy(); expect(within(lane('Sessions')).getByText('2')).toBeTruthy(); // The summary counts only lanes that answered. diff --git a/dashboard/src/workspaces/explorer/Lane.tsx b/dashboard/src/workspaces/explorer/Lane.tsx index 598bee0c85..9340a08e48 100644 --- a/dashboard/src/workspaces/explorer/Lane.tsx +++ b/dashboard/src/workspaces/explorer/Lane.tsx @@ -8,6 +8,7 @@ * worked. Nothing in a lane is computed from another lane. */ import { type KeyboardEvent, type ReactNode } from 'react'; +import { EvidencePattern } from '../../ui/EvidencePattern.tsx'; import { StateChip } from '../../ui/StateChip'; import { VirtualList } from '../../ui/VirtualList.tsx'; import { Highlight } from '../../ui/search/Highlight.tsx'; @@ -17,6 +18,7 @@ import { compactRelativeAge } from '../../ui/time.ts'; import { laneGrade } from './evidence.ts'; import { LANE_BY_ID, LANE_ICON } from './laneChrome.ts'; import { + laneEvidence, laneHits, laneStateDetail, laneStateKind, @@ -97,16 +99,24 @@ export function Lane({
- - {answered - ? read.state === 'partial' - ? 'loaded from an incomplete read; records were omitted' - : total != null - ? `loaded of ${total.toLocaleString()} matching reported` - : searching - ? 'loaded; no source total reported' - : 'shown from the overview endpoint' - : 'no count reported'} + + {/* How well the count is known, on the shared pattern axis: solid + * when the source reported a real denominator, hatched when rows + * arrived without one. Distinct from the record grade beside it. */} + {answered ? ( + + ) : null} + + {answered + ? read.state === 'partial' + ? 'loaded from an incomplete read; records were omitted' + : total != null + ? `loaded of ${total.toLocaleString()} matching reported` + : searching + ? 'loaded; no source total reported' + : 'shown from the overview endpoint' + : 'no count reported'} + {grade !== null ? ( {answered ? `${rows.length.toLocaleString()} shown` : '—'} {answered && read.hasMore === true ? ' · more rows remain past this page' : ''} + {/* The source's own freshness word and watermark, so a served page + * says how current it is rather than looking current by default. */} + {answered ? ( + + {` · ${read.freshness}`} + {read.watermark !== null ? ` @ ${read.watermark}` : ''} + + ) : null} {answered diff --git a/dashboard/src/workspaces/explorer/evidence.test.ts b/dashboard/src/workspaces/explorer/evidence.test.ts index 150b1f120c..14efdd3543 100644 --- a/dashboard/src/workspaces/explorer/evidence.test.ts +++ b/dashboard/src/workspaces/explorer/evidence.test.ts @@ -33,6 +33,8 @@ describe('laneGrade', () => { reportedTotal: 0, unreadableRows: 0, hasMore: false, + freshness: 'fresh', + watermark: null, }; const stale: ExplorerLaneReadModel = { state: 'stale', lane: 'code', errorCode: null, detail: null }; const pending: ExplorerLaneReadModel = { state: 'pending', lane: 'code', phase: 'reading' }; diff --git a/dashboard/src/workspaces/explorer/laneModel.test.ts b/dashboard/src/workspaces/explorer/laneModel.test.ts index 82d786ca5f..d1eb3f9269 100644 --- a/dashboard/src/workspaces/explorer/laneModel.test.ts +++ b/dashboard/src/workspaces/explorer/laneModel.test.ts @@ -191,6 +191,8 @@ describe('laneFromSourceProgress', () => { reportedTotal: 0, unreadableRows: 0, hasMore: false, + freshness: 'unknown', + watermark: null, }); expect(new Set([...states, readyEmpty.state]).size).toBe(4); }); @@ -248,9 +250,20 @@ describe('laneFromSourceProgress', () => { reportedTotal: null, unreadableRows: 0, hasMore: null, + freshness: 'unknown', + watermark: null, }); }); + it('carries the source\u2019s own freshness word and watermark verbatim', () => { + const fresh = laneFromSourceProgress( + 'code', + progress({ freshness: 'fresh', watermark: 'g-42' }), + [], + ); + expect(fresh).toMatchObject({ state: 'ready', freshness: 'fresh', watermark: 'g-42' }); + }); + it('reports rows it could not read instead of silently returning fewer', () => { const read = laneFromSourceProgress( 'code', diff --git a/dashboard/src/workspaces/explorer/laneModel.ts b/dashboard/src/workspaces/explorer/laneModel.ts index fa4a2e7067..cbd6e50137 100644 --- a/dashboard/src/workspaces/explorer/laneModel.ts +++ b/dashboard/src/workspaces/explorer/laneModel.ts @@ -92,6 +92,10 @@ export type ExplorerLaneReadModel = /** Whether the source said rows remain past this page; `null` when the * read carries no pagination at all (the browse overviews). */ readonly hasMore: boolean | null; + /** The source's own freshness word (`fresh`, `unknown`, …) and the + * watermark it answered from, verbatim. Never inferred. */ + readonly freshness: string; + readonly watermark: string | null; } /** The source answered with real rows but its own read reported omitted * records: what is shown is genuine and less than what exists. */ @@ -102,6 +106,8 @@ export type ExplorerLaneReadModel = readonly reportedTotal: number | null; readonly unreadableRows: number; readonly hasMore: boolean | null; + readonly freshness: string; + readonly watermark: string | null; readonly errorCode: string | null; readonly detail: string | null; } @@ -302,6 +308,8 @@ export function laneFromSourceProgress( reportedTotal: null, unreadableRows: 0, hasMore: null, + freshness: source.freshness, + watermark: source.watermark, }; } const hits = hitsForLane(lane, narrowPageRows(page), terms); @@ -312,6 +320,8 @@ export function laneFromSourceProgress( reportedTotal: page.total, unreadableRows: page.rows.length - hits.length, hasMore: page.next_offset !== null, + freshness: source.freshness, + watermark: source.watermark, }; } case 'partial': { @@ -324,6 +334,8 @@ export function laneFromSourceProgress( reportedTotal: page?.total ?? null, unreadableRows: page === null ? 0 : page.rows.length - hits.length, hasMore: page === null ? null : page.next_offset !== null, + freshness: source.freshness, + watermark: source.watermark, errorCode: source.error_code, detail: source.message, }; @@ -450,6 +462,9 @@ export function browseLane( } const rows = rowsOf(result.envelope.payload); const hits = hitsForLane(lane, rows, terms); + // The overview carries no per-source freshness; the envelope's own + // freshness reading is the daemon's statement about this read. + const freshness = result.envelope.freshness; return { state: 'ready', lane, @@ -457,6 +472,8 @@ export function browseLane( reportedTotal: null, unreadableRows: rows.length - hits.length, hasMore: null, + freshness: freshness.state, + watermark: freshness.watermark, }; } From 77e103385f6eaa89bf5304efcad54926d697cfd8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 03:00:35 +0000 Subject: [PATCH 3/6] fix(explorer): report scope refusal in the run register Co-authored-by: Zack Jackson --- .../explorer/ExplorerPage.dom.test.tsx | 4 +++ .../src/workspaces/explorer/ExplorerPage.tsx | 1 + .../src/workspaces/explorer/controller.ts | 5 +++ dashboard/src/workspaces/explorer/panels.tsx | 36 +++++++++++++++---- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx b/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx index 00535c1b98..bde8758569 100644 --- a/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx +++ b/dashboard/src/workspaces/explorer/ExplorerPage.dom.test.tsx @@ -440,6 +440,10 @@ describe('ExplorerPage scope truth', () => { within(lane('Code')).getAllByText(/Other project is not the active project/).length, ).toBeGreaterThan(0); expect(within(lane('Code')).getByText('Read-only scope')).toBeTruthy(); + // The run register carries the same refusal rather than a pending admission. + const register = document.querySelector('[data-run-register]'); + expect(register?.getAttribute('data-run-state')).toBe('locked'); + expect(within(register as HTMLElement).getByText(/no run created/)).toBeTruthy(); expect( fetchImpl.mock.calls.some(([input]) => String(input).includes('/api/explorer/queries')), ).toBe(false); diff --git a/dashboard/src/workspaces/explorer/ExplorerPage.tsx b/dashboard/src/workspaces/explorer/ExplorerPage.tsx index fe3d94a401..74f3400383 100644 --- a/dashboard/src/workspaces/explorer/ExplorerPage.tsx +++ b/dashboard/src/workspaces/explorer/ExplorerPage.tsx @@ -131,6 +131,7 @@ export function ExplorerPage() { | undefined, +): { kind: DomainStateKind; detail: string } { + switch (writability.state) { + case 'read_only': + return { kind: 'locked', detail: 'no run created: the scope is read-only' }; + case 'unknown': + return { kind: 'unknown', detail: 'no run created until the scope is checked' }; + case 'writable': + return result?.outcome === 'transport' + ? { kind: result.state, detail: result.detail ?? 'coordinator response unavailable' } + : { kind: 'loading', detail: 'admitting the source plan' }; + default: { + const exhaustive: never = writability; + return exhaustive; + } + } +} + /** * The run readout. Progress is counted in sources concluded, because that is * the only figure with a real denominator: sources report incommensurable @@ -21,6 +45,7 @@ import { runStateKind } from './laneModel.ts'; export function RunRegister({ result, run, + writability, progress, cancelling, onCancel, @@ -28,23 +53,20 @@ export function RunRegister({ }: { result: EnvelopeResult | undefined; run: ExplorerQueryRunV1 | undefined; + writability: ScopeWritability; progress: RunProgress | null; cancelling: boolean; onCancel: (() => void) | undefined; className?: string; }) { if (!run) { - // One reading of the result, not two: a transport failure is rendered - // from its state and detail; anything else is the plan being admitted. - const blocked = - result?.outcome === 'transport' - ? { kind: result.state, detail: result.detail ?? 'coordinator response unavailable' } - : { kind: 'loading' as const, detail: 'admitting the source plan' }; + const blocked = noRunReading(writability, result); return (
Run From 8a84e568c907a406a528af8b9b4d00dd6d49cf76 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 03:37:15 +0000 Subject: [PATCH 4/6] fix(explorer): clip the lane footer watermark Co-authored-by: Zack Jackson --- dashboard/src/workspaces/explorer/Lane.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dashboard/src/workspaces/explorer/Lane.tsx b/dashboard/src/workspaces/explorer/Lane.tsx index 9340a08e48..60cb2137e3 100644 --- a/dashboard/src/workspaces/explorer/Lane.tsx +++ b/dashboard/src/workspaces/explorer/Lane.tsx @@ -29,6 +29,12 @@ import type { Hit } from './model.ts'; /** Fixed row height shared with the virtualizer: title, location, meta. */ export const LANE_ROW_HEIGHT = 60; +/** A code-graph generation watermark is a long dotted digest; the footer + * shows enough to tell two apart and carries the whole value as a title. */ +function clipWatermark(watermark: string): string { + return watermark.length <= 28 ? watermark : `${watermark.slice(0, 27)}…`; +} + /** Marks each lane's row region so the grid can traverse lanes with the * horizontal arrows without knowing how a lane lays itself out. */ export const LANE_LIST_ATTR = 'data-lane-list'; @@ -154,9 +160,9 @@ export function Lane({ {/* The source's own freshness word and watermark, so a served page * says how current it is rather than looking current by default. */} {answered ? ( - + {` · ${read.freshness}`} - {read.watermark !== null ? ` @ ${read.watermark}` : ''} + {read.watermark !== null ? ` @ ${clipWatermark(read.watermark)}` : ''} ) : null} From b6e309a134eeb80496c103880b0e1092d2bbac95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 03:47:15 +0000 Subject: [PATCH 5/6] fix(explorer): keep facet selects inside the aperture at 320px Co-authored-by: Zack Jackson --- .../src/workspaces/explorer/ExplorerPage.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/dashboard/src/workspaces/explorer/ExplorerPage.tsx b/dashboard/src/workspaces/explorer/ExplorerPage.tsx index 74f3400383..1f3e9a8667 100644 --- a/dashboard/src/workspaces/explorer/ExplorerPage.tsx +++ b/dashboard/src/workspaces/explorer/ExplorerPage.tsx @@ -160,7 +160,8 @@ export function ExplorerPage() { return ( explorer.setFacet(value === '' ? null : { lane: laneId, value }) @@ -246,25 +247,34 @@ function asLaneId(value: string): LaneId | null { } /** A native select behind an engraved legend, so every filter is one control - * with one accessible name and the platform's own keyboard behaviour. */ + * with one accessible name and the platform's own keyboard behaviour. + * + * `name` widens the accessible name past the visible legend when the legend + * alone would be ambiguous (three lanes each have a facet); the visible text + * stays inside the name, so label-in-name holds. The select is capped rather + * than sized to its widest option, which at 320px pushed the whole register + * past the aperture's edge. */ function FilterSelect({ label, + name, value, onChange, options, }: { label: string; + name?: string; value: string; onChange: (value: string) => void; options: readonly { value: string; label: ReactNode }[]; }) { return ( -