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
Binary file modified dashboard/audit-baselines/agents__dark__1440.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 dashboard/audit-baselines/agents__dark__320.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 dashboard/audit-baselines/agents__dark__768.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 dashboard/audit-baselines/agents__light__1440.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 dashboard/audit-baselines/agents__light__320.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 dashboard/audit-baselines/agents__light__768.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 dashboard/audit-baselines/automations__dark__1440.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 dashboard/audit-baselines/automations__dark__320.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 dashboard/audit-baselines/automations__dark__768.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 dashboard/audit-baselines/automations__light__1440.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 dashboard/audit-baselines/automations__light__320.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 dashboard/audit-baselines/automations__light__768.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 dashboard/audit-baselines/brain__dark__1440.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 dashboard/audit-baselines/brain__dark__320.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 dashboard/audit-baselines/brain__dark__768.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 dashboard/audit-baselines/brain__light__1440.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 dashboard/audit-baselines/brain__light__320.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 dashboard/audit-baselines/brain__light__768.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 dashboard/audit-baselines/code__dark__1440.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 dashboard/audit-baselines/code__dark__320.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 dashboard/audit-baselines/code__dark__768.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 dashboard/audit-baselines/code__light__1440.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 dashboard/audit-baselines/code__light__320.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 dashboard/audit-baselines/code__light__768.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 dashboard/audit-baselines/costs__dark__1440.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 dashboard/audit-baselines/costs__dark__320.png
Binary file modified dashboard/audit-baselines/costs__dark__768.png
Binary file modified dashboard/audit-baselines/costs__light__1440.png
Binary file modified dashboard/audit-baselines/costs__light__320.png
Binary file modified dashboard/audit-baselines/costs__light__768.png
Binary file modified dashboard/audit-baselines/delivery__dark__1440.png
Binary file modified dashboard/audit-baselines/delivery__dark__320.png
Binary file modified dashboard/audit-baselines/delivery__dark__768.png
Binary file modified dashboard/audit-baselines/delivery__light__1440.png
Binary file modified dashboard/audit-baselines/delivery__light__320.png
Binary file modified dashboard/audit-baselines/delivery__light__768.png
Binary file modified dashboard/audit-baselines/explorer__dark__1440.png
Binary file modified dashboard/audit-baselines/explorer__dark__320.png
Binary file modified dashboard/audit-baselines/explorer__dark__768.png
Binary file modified dashboard/audit-baselines/explorer__light__1440.png
Binary file modified dashboard/audit-baselines/explorer__light__320.png
Binary file modified dashboard/audit-baselines/explorer__light__768.png
Binary file modified dashboard/audit-baselines/knowledge__dark__1440.png
Binary file modified dashboard/audit-baselines/knowledge__dark__320.png
Binary file modified dashboard/audit-baselines/knowledge__dark__768.png
Binary file modified dashboard/audit-baselines/knowledge__light__1440.png
Binary file modified dashboard/audit-baselines/knowledge__light__320.png
Binary file modified dashboard/audit-baselines/knowledge__light__768.png
Binary file modified dashboard/audit-baselines/loom__dark__1440.png
Binary file modified dashboard/audit-baselines/loom__dark__320.png
Binary file modified dashboard/audit-baselines/loom__dark__768.png
Binary file modified dashboard/audit-baselines/loom__light__1440.png
Binary file modified dashboard/audit-baselines/loom__light__320.png
Binary file modified dashboard/audit-baselines/loom__light__768.png
Binary file modified dashboard/audit-baselines/observatory__dark__1440.png
Binary file modified dashboard/audit-baselines/observatory__dark__320.png
Binary file modified dashboard/audit-baselines/observatory__dark__768.png
Binary file modified dashboard/audit-baselines/observatory__light__1440.png
Binary file modified dashboard/audit-baselines/observatory__light__320.png
Binary file modified dashboard/audit-baselines/observatory__light__768.png
Binary file modified dashboard/audit-baselines/sessions__dark__1440.png
Binary file modified dashboard/audit-baselines/sessions__dark__320.png
Binary file modified dashboard/audit-baselines/sessions__dark__768.png
Binary file modified dashboard/audit-baselines/sessions__light__1440.png
Binary file modified dashboard/audit-baselines/sessions__light__320.png
Binary file modified dashboard/audit-baselines/sessions__light__768.png
Binary file modified dashboard/audit-baselines/settings__dark__1440.png
Binary file modified dashboard/audit-baselines/settings__dark__320.png
Binary file modified dashboard/audit-baselines/settings__dark__768.png
Binary file modified dashboard/audit-baselines/settings__light__1440.png
Binary file modified dashboard/audit-baselines/settings__light__320.png
Binary file modified dashboard/audit-baselines/settings__light__768.png
Binary file added dashboard/audit-baselines/work__dark__1440.png
Binary file added dashboard/audit-baselines/work__dark__320.png
Binary file added dashboard/audit-baselines/work__dark__768.png
Binary file added dashboard/audit-baselines/work__light__1440.png
Binary file added dashboard/audit-baselines/work__light__320.png
Binary file added dashboard/audit-baselines/work__light__768.png
677 changes: 677 additions & 0 deletions dashboard/e2e/responsive.ts

Large diffs are not rendered by default.

38 changes: 7 additions & 31 deletions dashboard/package-lock.json

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

2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"live:multiproject": "tsx stories/live-multiproject.ts"
},
"dependencies": {
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/ibm-plex-sans": "^5.3.0",
"@fontsource/chakra-petch": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@radix-ui/react-dialog": "^1.1.4",
"@tanstack/react-query": "^5.62.0",
Expand Down
35 changes: 35 additions & 0 deletions dashboard/src/app/RouteChunkBoundary.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,39 @@ describe('RouteChunkBoundary', () => {
expect(screen.queryByText('Offline')).toBeNull();
expect(loadCalls).toBeGreaterThan(callsAfterFailure);
});

/**
* Every route mounts this same component type at the same Outlet slot, so a
* client-side navigation is a PROP update, not a remount. The boundary must
* swap to the new loader's page; pinning `lazy(load)` in the constructor
* left the whole app stuck on whichever workspace rendered first.
*/
it('swaps to the new page when the load prop changes without a remount', async () => {
const loadBrain = () =>
Promise.resolve({ default: () => <div>brain surface</div> });
const loadCode = () =>
Promise.resolve({ default: () => <div>code surface</div> });

const { rerender } = render(<RouteChunkBoundary load={loadBrain} />);
expect(await screen.findByText('brain surface')).toBeTruthy();

rerender(<RouteChunkBoundary load={loadCode} />);
expect(await screen.findByText('code surface')).toBeTruthy();
expect(screen.queryByText('brain surface')).toBeNull();
});

it('clears a previous route failure when navigating to a different loader', async () => {
const error = new Error('Failed to fetch dynamically imported module');
error.name = 'ChunkLoadError';
const loadBroken = () => Promise.reject(error);
const loadHealthy = () =>
Promise.resolve({ default: () => <div>healthy surface</div> });

const { rerender } = render(<RouteChunkBoundary load={loadBroken} />);
expect(await screen.findByText('Offline')).toBeTruthy();

rerender(<RouteChunkBoundary load={loadHealthy} />);
expect(await screen.findByText('healthy surface')).toBeTruthy();
expect(screen.queryByText('Offline')).toBeNull();
});
});
55 changes: 47 additions & 8 deletions dashboard/src/app/RouteChunkBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,38 +89,77 @@ type RouteChunkBoundaryProps = {
type RouteChunkBoundaryState = {
error: Error | null;
attempt: number;
/** The loader the current `error`/`attempt` belong to. Every route mounts
* this same component type at the same Outlet position, so React reconciles
* a navigation as a PROP change — the boundary must notice the new loader
* itself rather than assuming a fresh mount. */
loadedFor: RouteChunkLoader;
};

/**
* Per-route lazy boundary. React `lazy()` caches a rejected import on that
* instance, so retry allocates a new `lazy(load)` and remounts it.
* One lazy component per loader, shared across renders and navigations.
*
* `lazy()` memoizes its resolved module on the instance, so a per-loader cache
* means revisiting a workspace renders it synchronously instead of suspending
* again. A rejected import is also cached on the instance, which is why
* `retry` evicts the entry and allocates a fresh one.
*/
const CHUNKS = new WeakMap<RouteChunkLoader, LazyExoticComponent<ComponentType>>();

function lazyFor(load: RouteChunkLoader): LazyExoticComponent<ComponentType> {
let page = CHUNKS.get(load);
if (page === undefined) {
page = lazy(load);
CHUNKS.set(load, page);
}
return page;
}

/**
* Per-route lazy boundary.
*
* THE PROP CAN CHANGE WITHOUT A REMOUNT. React Router keeps the `<Outlet>`
* slot's component type identical across workspace navigations, so switching
* from `/brain` to `/code` updates `load` on the existing instance. The first
* version of this class computed `lazy(props.load)` once in its constructor,
* which pinned every client-side navigation to whichever workspace loaded
* first — the URL and the nav rail moved, the content never did.
*/
export class RouteChunkBoundary extends Component<
RouteChunkBoundaryProps,
RouteChunkBoundaryState
> {
state: RouteChunkBoundaryState = { error: null, attempt: 0 };
private Page: LazyExoticComponent<ComponentType>;

constructor(props: RouteChunkBoundaryProps) {
super(props);
this.Page = lazy(props.load);
this.state = { error: null, attempt: 0, loadedFor: props.load };
}

static getDerivedStateFromError(error: unknown): Pick<RouteChunkBoundaryState, 'error'> {
return { error: asError(error) };
}

static getDerivedStateFromProps(
props: RouteChunkBoundaryProps,
state: RouteChunkBoundaryState,
): Partial<RouteChunkBoundaryState> | null {
// A navigation to a different route: drop the previous route's failure so
// one broken chunk does not paint every workspace unavailable.
if (props.load !== state.loadedFor) {
return { error: null, attempt: 0, loadedFor: props.load };
}
return null;
}

private retry = () => {
this.Page = lazy(this.props.load);
CHUNKS.delete(this.props.load);
this.setState((state) => ({ error: null, attempt: state.attempt + 1 }));
};

render(): ReactNode {
if (this.state.error) {
return <RouteChunkUnavailable error={this.state.error} onRetry={this.retry} />;
}
const Page = this.Page;
const Page = lazyFor(this.props.load);
return (
<Suspense fallback={<ChunkFallback />}>
<Page key={this.state.attempt} />
Expand Down
37 changes: 23 additions & 14 deletions dashboard/src/app/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,35 @@
* is a plain data module with no JSX so the shell chrome and the pages can both
* read it without an import cycle through the router.
*/
/** Which register of the tactical dock a channel files under. `workspace` is
* where the operator reads and explores; `ops` is where the system reports on
* itself and its deliveries; `config` is the one channel that changes the
* instrument instead of reading it. */
export type ChannelGroup = 'workspace' | 'ops' | 'config';

export interface Channel {
readonly path: string;
readonly label: string;
readonly group: ChannelGroup;
}

export const CHANNELS: readonly Channel[] = [
{ path: 'brain', label: 'Brain' },
{ path: 'explorer', label: 'Explorer' },
{ path: 'loom', label: 'Loom' },
{ path: 'sessions', label: 'Sessions' },
{ path: 'agents', label: 'Agents' },
{ path: 'code', label: 'Code' },
{ path: 'knowledge', label: 'Knowledge' },
{ path: 'delivery', label: 'Delivery' },
{ path: 'automations', label: 'Automations' },
{ path: 'observatory', label: 'Observatory' },
{ path: 'costs', label: 'Costs' },
{ path: 'settings', label: 'Settings' },
{ path: 'work', label: 'Work' },
{ path: 'workflows', label: 'Workflows' },
{ path: 'brain', label: 'Brain', group: 'workspace' },
{ path: 'explorer', label: 'Explorer', group: 'workspace' },
{ path: 'loom', label: 'Loom', group: 'workspace' },
{ path: 'sessions', label: 'Sessions', group: 'workspace' },
{ path: 'agents', label: 'Agents', group: 'workspace' },
{ path: 'code', label: 'Code', group: 'workspace' },
{ path: 'knowledge', label: 'Knowledge', group: 'workspace' },
{ path: 'delivery', label: 'Delivery', group: 'ops' },
{ path: 'automations', label: 'Automations', group: 'ops' },
{ path: 'observatory', label: 'Observatory', group: 'ops' },
{ path: 'costs', label: 'Costs', group: 'ops' },
// Settings keeps channel 12: the number is the channel's identity, so
// regrouping the rail must never renumber an instrument.
{ path: 'settings', label: 'Settings', group: 'config' },
{ path: 'work', label: 'Work', group: 'ops' },
{ path: 'workflows', label: 'Workflows', group: 'ops' },
] as const;

/** Zero-padded channel number for a workspace path (`code` → `06`). Unknown
Expand Down
47 changes: 38 additions & 9 deletions dashboard/src/app/shell/NavRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { NavLink } from 'react-router';
import type { StorageFindingKindStatusV1 } from '../../contracts/generated.ts';
import { useStorageFindings } from '../../data/query/storageFindings.ts';
import { cn } from '../../ui/cn';
import { CHANNELS, channelNumber } from '../channels.ts';
import { CHANNELS, channelNumber, type ChannelGroup } from '../channels.ts';

const ICONS: Record<string, LucideIcon> = {
brain: Brain,
Expand All @@ -40,7 +40,13 @@ const ICONS: Record<string, LucideIcon> = {
workflows: Waypoints,
};

const MAIN = CHANNELS.filter((channel) => channel.path !== 'settings');
/** The dock's two scrolling registers, in rail order. `config` (Settings) is
* pinned to the rail's foot rather than filed here. Channels keep their
* numbers — grouping arranges the dock, it never renumbers an instrument. */
const REGISTERS: readonly { group: ChannelGroup; label: string }[] = [
{ group: 'workspace', label: 'Workspace' },
{ group: 'ops', label: 'Ops' },
];

/**
* What the app-wide Doctor dot is allowed to say.
Expand Down Expand Up @@ -238,18 +244,41 @@ export function NavRail() {
</span>
</div>
<div className="min-h-0 flex-1 overflow-auto">
{MAIN.map((channel) => (
<RailLink
key={channel.path}
path={channel.path}
label={channel.label}
health={channel.path === 'observatory' ? health : undefined}
/>
{REGISTERS.map((register) => (
<section key={register.group} aria-label={register.label}>
<RailRegisterLabel label={register.label} />
{CHANNELS.filter((channel) => channel.group === register.group).map(
(channel) => (
<RailLink
key={channel.path}
path={channel.path}
label={channel.label}
health={channel.path === 'observatory' ? health : undefined}
/>
),
)}
</section>
))}
</div>
<div className="shrink-0 border-t border-edge-subtle">
<RailRegisterLabel label="Config" />
<RailLink path="settings" label="Settings" />
</div>
</nav>
);
}

/** A register divider: the engraved group stamp over its channels. Collapsed
* rails keep the hairline so the grouping survives at icon width, they just
* lose the word. */
function RailRegisterLabel({ label }: { label: string }) {
return (
<div
aria-hidden
className="flex h-6 items-center gap-2 border-b border-edge-subtle bg-surface-0 px-3.5 max-md:justify-center max-md:px-0"
>
<span className="td-legend text-3xs text-text-muted max-md:hidden">{label}</span>
<span aria-hidden className="td-rule opacity-60" />
</div>
);
}
11 changes: 9 additions & 2 deletions dashboard/src/app/shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Outlet } from 'react-router';
import { NavRail } from './NavRail';
import { ScopeBar } from './ScopeBar';
import { ScopeUrlSync } from '../../data/scope/UrlSync.tsx';
import { QueryActivityStatus, StatusStrip } from './StatusStrip';
import { QueryActivityStatus, SourceProvenance, StatusStrip } from './StatusStrip';

const CommandPalette = lazy(() =>
import('./CommandPalette').then((m) => ({ default: m.CommandPalette })),
Expand Down Expand Up @@ -63,7 +63,14 @@ export function Shell() {
>
<Outlet />
</main>
<StatusStrip queryActivity={<QueryActivityStatus />} />
<StatusStrip
queryActivity={
<>
<SourceProvenance />
<QueryActivityStatus />
</>
}
/>
</div>
</div>
);
Expand Down
Loading
Loading