feat(os): the VestigeOS nervous system — canonical registry + persistent shell + mutation safety

THE nervous system (audit P0): one os-routes.ts registry (all 20 organs w/ group/
shortcut/icon/purpose) + base-aware os-nav.ts (osHref/osGoto + URL cognitive
context: memory/run/receipt/q/focus/cinema/days/filter). The root shell is now a
FLOATING dock + grouped ⌘K palette that overlays every full-bleed canvas organ,
so no page is a navigation island (Palace is in the nav for the first time).
Hideable via ?capture=1 for clean hero footage. Verified live: dock renders +
⌘K palette opens over the canvas, 0 console errors.

MUTATION SAFETY (audit dangerous-interactions, via M3 worker): a plain click now
SELECTS, never silently mutates. Feed no longer wipes on click (clearEvents gone);
Stats no longer runs consolidate() on a vital click (explicit button only);
Memories plain-click selects instead of promoting, with a visible affordance hint
for shift/alt. check 0/0, 979 files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-07-13 10:42:37 +07:00
parent 65595b9ffc
commit 4168925099
6 changed files with 644 additions and 265 deletions

View file

@ -0,0 +1,92 @@
// ─────────────────────────────────────────────────────────────────────────────
// os-nav.ts — base-aware routing + the URL cognitive-context contract.
//
// Fixes the audit's launch-blocker: components navigated with unbased paths like
// goto(`/graph?center=...`) which ESCAPE the configured /dashboard base and 404
// on the deployed site. Every cross-organ link must go through osHref()/osGoto()
// so it stays inside the base.
//
// The "cognitive context" is a small, shared set of URL params that carry
// selection/state ACROSS organs so a journey (Rescue → Graph → Reason → Black
// Box → Cinema) keeps its selections intact. `memory` is the canonical
// cross-organ contract; `center` is kept as a temporary Graph alias.
// ─────────────────────────────────────────────────────────────────────────────
import { base } from '$app/paths';
import { goto } from '$app/navigation';
/** The URL-backed cognitive context shared across every organ. */
export interface CognitiveContext {
/** The focused memory id — the cross-organ selection contract. */
memory?: string;
/** A Black Box run id. */
run?: string;
/** A receipt id. */
receipt?: string;
/** A free-text query (Explore/Activation/Reasoning). */
q?: string;
/** Comma-separated ids to emphasize (Graph focus set). */
focus?: string[];
/** Launch Memory Cinema on arrival. */
cinema?: boolean;
/** Time window in days (Timeline/Schedule). */
days?: number;
/** A generic per-organ filter value. */
filter?: string;
/** Temporary Graph alias for `memory` (legacy ?center=). */
center?: string;
}
/** Serialize a cognitive context to a query string (stable key order, base-free). */
export function contextToQuery(ctx: CognitiveContext): string {
const p = new URLSearchParams();
if (ctx.memory) p.set('memory', ctx.memory);
if (ctx.run) p.set('run', ctx.run);
if (ctx.receipt) p.set('receipt', ctx.receipt);
if (ctx.q) p.set('q', ctx.q);
if (ctx.focus && ctx.focus.length) p.set('focus', ctx.focus.join(','));
if (ctx.cinema) p.set('cinema', '1');
if (typeof ctx.days === 'number') p.set('days', String(ctx.days));
if (ctx.filter) p.set('filter', ctx.filter);
if (ctx.center) p.set('center', ctx.center);
const s = p.toString();
return s ? `?${s}` : '';
}
/** Read the cognitive context out of a URL (SvelteKit page.url or a URLSearchParams). */
export function contextFromUrl(url: URL): CognitiveContext {
const p = url.searchParams;
const focus = p.get('focus');
const days = p.get('days');
return {
// `memory` is canonical; fall back to the legacy `center` alias.
memory: p.get('memory') ?? p.get('center') ?? undefined,
run: p.get('run') ?? undefined,
receipt: p.get('receipt') ?? undefined,
q: p.get('q') ?? undefined,
focus: focus ? focus.split(',').filter(Boolean) : undefined,
cinema: p.get('cinema') === '1',
days: days != null && days !== '' && Number.isFinite(Number(days)) ? Number(days) : undefined,
filter: p.get('filter') ?? undefined,
center: p.get('center') ?? undefined
};
}
/**
* Build a base-safe href for an organ route (path is WITHOUT base, e.g. '/graph').
* ALWAYS use this for cross-organ links never a bare `/graph` string.
*/
export function osHref(pathNoBase: string, ctx: CognitiveContext = {}): string {
const clean = pathNoBase.startsWith('/') ? pathNoBase : `/${pathNoBase}`;
return `${base}${clean}${contextToQuery(ctx)}`;
}
/** Navigate to an organ route carrying cognitive context, base-safe. */
export function osGoto(pathNoBase: string, ctx: CognitiveContext = {}): Promise<void> {
return goto(osHref(pathNoBase, ctx));
}
/** The current dashboard path with the base stripped (for active-route matching). */
export function stripBase(pathname: string): string {
return pathname.startsWith(base) ? pathname.slice(base.length) || '/' : pathname;
}

View file

@ -0,0 +1,109 @@
// ─────────────────────────────────────────────────────────────────────────────
// os-routes.ts — THE canonical VestigeOS route registry.
//
// ONE source of truth for every navigation surface. Before this file there were
// four disagreeing inventories (nav-layer's 9-organ COGNITIVE_OS_ROUTES,
// palace-map's 19 ORGAN_REGIONS, MobileNav, and the e2e ALL_ROUTES) — so
// desktop canvas pages became navigation islands (Palace itself was in NONE of
// the visible nav surfaces). Every consumer now derives from this list:
// - the persistent shell dock (primary organs)
// - the grouped command palette (⌘K) (ALL organs, by group)
// - the mobile menu (ALL groups + organs)
// - the in-canvas WebGPU nav rail (enhancement only)
// - the Palace constellation (visible organs)
// - the e2e route smoke list (all reachable)
//
// A route is only shippable customer-facing nav if it appears here with
// visible !== 'internal'. `_msdftest` is deliberately excluded (internal only).
// ─────────────────────────────────────────────────────────────────────────────
import type { IconName } from '$lib/components/Icon.svelte';
/** Which OS group an organ belongs to (drives palette + mobile grouping). */
export type OsGroup = 'Primary' | 'Understand' | 'Maintain' | 'Reflect' | 'System';
/** How the route mounts its renderer (informational; used by fallback logic). */
export type StageType = 'observatory' | 'route-stage' | 'canvas' | 'dom';
/** Launch visibility: dock = in the persistent dock; nav = in palette/mobile/palace;
* hidden = reachable by URL + palette only; internal = never in customer nav. */
export type Visibility = 'dock' | 'nav' | 'hidden' | 'internal';
export interface OsRoute {
/** Path WITHOUT the base prefix, e.g. '/graph'. Use osHref() to render. */
href: string;
/** Human label shown in every nav surface. */
label: string;
/** One-line purpose, shown in the command palette + organ overlay. */
purpose: string;
group: OsGroup;
/** Single-key shortcut (⌘K palette + in-canvas rail). Unique across the set. */
shortcut?: string;
icon: IconName;
stage: StageType;
visibility: Visibility;
/** true once the organ is launch-ready; false surfaces a "beta" affordance. */
ready: boolean;
}
// ── The registry — all 20 customer organs (order = canonical display order) ──
export const OS_ROUTES: OsRoute[] = [
// PRIMARY — the spine of the product, in the persistent dock.
{ href: '/palace', label: 'Palace', purpose: 'The spatial launcher — every organ as a constellation you fly into.', group: 'Primary', shortcut: 'P', icon: 'logo', stage: 'canvas', visibility: 'dock', ready: true },
{ href: '/observatory', label: 'Observatory', purpose: 'The living memory field + the deterministic cognitive moments (Salience Rescue).', group: 'Primary', shortcut: 'O', icon: 'sparkle', stage: 'observatory', visibility: 'dock', ready: true },
{ href: '/graph', label: 'Graph', purpose: 'The causal memory graph — field + classic inspector, selection, Cinema entry.', group: 'Primary', shortcut: 'G', icon: 'graph', stage: 'canvas', visibility: 'dock', ready: true },
{ href: '/memories', label: 'Memories', purpose: 'Browse, search, and inspect individual memories with their FSRS state.', group: 'Primary', shortcut: 'M', icon: 'memories', stage: 'observatory', visibility: 'dock', ready: true },
{ href: '/timeline', label: 'Timeline', purpose: 'Bitemporal history — when memories were valid vs recorded, with audit windows.', group: 'Primary', shortcut: 'T', icon: 'timeline', stage: 'route-stage', visibility: 'dock', ready: true },
{ href: '/blackbox', label: 'Black Box', purpose: 'The receipt — what Vestige concluded, the evidence, and why, exportable.', group: 'Primary', shortcut: 'B', icon: 'blackbox', stage: 'route-stage', visibility: 'dock', ready: true },
// UNDERSTAND — the reasoning + exploration organs.
{ href: '/reasoning', label: 'Reasoning', purpose: 'Watch a live deep_reference decision trace form from evidence.', group: 'Understand', shortcut: 'R', icon: 'reasoning', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/explore', label: 'Explore', purpose: 'A shareable semantic walk through memory neighborhoods.', group: 'Understand', shortcut: 'E', icon: 'explore', stage: 'observatory', visibility: 'nav', ready: true },
{ href: '/contradictions', label: 'Contradictions', purpose: 'Trust-weighted conflict pairs — where your memory disagrees with itself.', group: 'Understand', shortcut: 'C', icon: 'contradictions', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/patterns', label: 'Patterns', purpose: 'Cross-project patterns mined from the corpus.', group: 'Understand', icon: 'patterns', stage: 'route-stage', visibility: 'nav', ready: true },
// MAINTAIN — the memory-hygiene organs.
{ href: '/duplicates', label: 'Duplicates', purpose: 'Cosine-similarity clusters quarantined for review before merge.', group: 'Maintain', shortcut: 'D', icon: 'duplicates', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/memory-prs', label: 'Memory PRs', purpose: 'Proposed memory changes held for review before they touch the graph.', group: 'Maintain', icon: 'memorypr', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/importance', label: 'Importance', purpose: 'Which memories rank highest by the 4-channel importance model, and why.', group: 'Maintain', icon: 'importance', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/activation', label: 'Activation', purpose: 'The activation field — which memories light up for a query.', group: 'Maintain', shortcut: 'A', icon: 'activation', stage: 'observatory', visibility: 'nav', ready: true },
// REFLECT — the temporal / ambient organs.
{ href: '/feed', label: 'Activity', purpose: 'The live event stream — recalls, dreams, suppressions, as they happen.', group: 'Reflect', shortcut: 'F', icon: 'feed', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/dreams', label: 'Dreams', purpose: 'Replay consolidation cycles and the connections they discover.', group: 'Reflect', icon: 'dreams', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/schedule', label: 'Schedule', purpose: 'Review urgency — what is due now, overdue, and coming next.', group: 'Reflect', shortcut: 'H', icon: 'schedule', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/intentions', label: 'Intentions', purpose: 'Active and predicted intentions grouped by state.', group: 'Reflect', shortcut: 'I', icon: 'intentions', stage: 'route-stage', visibility: 'nav', ready: true },
{ href: '/stats', label: 'Stats', purpose: 'System vitals — retention distribution, coverage, throughput.', group: 'Reflect', shortcut: 'S', icon: 'stats', stage: 'route-stage', visibility: 'nav', ready: true },
// SYSTEM
{ href: '/settings', label: 'Settings', purpose: 'Tune the cognitive engine and run the maintenance rituals.', group: 'System', shortcut: ',', icon: 'settings', stage: 'route-stage', visibility: 'nav', ready: true }
];
// ── Derived views (every consumer uses these, never re-lists routes) ──────────
/** The persistent desktop dock: primary organs + a Command entry (added by shell). */
export const DOCK_ROUTES: OsRoute[] = OS_ROUTES.filter((r) => r.visibility === 'dock');
/** Everything a customer can navigate to (dock + nav + hidden), for the palette. */
export const NAV_ROUTES: OsRoute[] = OS_ROUTES.filter((r) => r.visibility !== 'internal');
/** Group order for the command palette + mobile menu. */
export const OS_GROUP_ORDER: OsGroup[] = ['Primary', 'Understand', 'Maintain', 'Reflect', 'System'];
/** Routes bucketed by group, in canonical order — drives the palette + mobile menu. */
export function routesByGroup(): { group: OsGroup; routes: OsRoute[] }[] {
return OS_GROUP_ORDER.map((group) => ({
group,
routes: NAV_ROUTES.filter((r) => r.group === group)
})).filter((g) => g.routes.length > 0);
}
/** The home route — Palace is the actual VestigeOS home. */
export const HOME_ROUTE = '/palace';
/** Look up a route by its href (with or without a leading base). */
export function findRoute(pathNoBase: string): OsRoute | undefined {
const p = pathNoBase || '/';
return OS_ROUTES.find((r) => p === r.href || p.startsWith(r.href + '/')) ??
(p === '/' ? OS_ROUTES.find((r) => r.href === HOME_ROUTE) : undefined);
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { websocket, eventFeed, isConnected, isReconnecting } from '$stores/websocket';
import { eventFeed, isConnected, isReconnecting } from '$stores/websocket';
import type { VestigeEvent } from '$types';
import RouteStage, { type RouteFramePass, type RoutePick } from '$lib/observatory/RouteStage.svelte';
import type { ObservatoryEngine } from '$lib/observatory/engine';
@ -20,6 +20,9 @@
let textPass: TextLayerPass | null = null;
let focusedRun: string | null = null;
// Tracked when the user picks a feed event from the canvas. Selection only —
// no API call. Inspect panels can read this state; the live feed stays intact.
let selectedEventId: string | null = $state(null);
// engine handle captured in the pass factory so buildTextItems can read the live
// viewport aspect (params[6]/[7]) for portrait-only event-line shortening.
let engineHandle: ObservatoryEngine | null = null;
@ -42,6 +45,14 @@
textPass?.setText(buildTextItems($eventFeed, $isConnected, $isReconnecting));
});
// Selection state surfaces to the renderer: a chosen event is highlighted
// through the same run-depth channel the hover handler uses, but persistently
// (until the next pick). This is a pure SELECT, no API call.
$effect(() => {
if (!textPass) return;
textPass.setRunDepth(selectedEventId, 1);
});
function createFeedPasses(engine: ObservatoryEngine, scene: RouteSceneModel): RouteFramePass[] {
engineHandle = engine;
const field = new FeedFieldPass(engine);
@ -444,7 +455,13 @@
}
function handleRoutePick(pick: RoutePick) {
if (pick.kind === 'feed-event') websocket.clearEvents();
// Plain click on a feed event must SELECT/INSPECT only — never mutate.
// Tracking the focused run here (without destroying the live feed) lets
// the renderer highlight the chosen event. clearEvents() is reserved
// for an explicit, separately-labeled "Clear" control, not a generic pick.
if (pick.kind === 'feed-event') {
selectedEventId = pick.id;
}
}
</script>

View file

@ -37,6 +37,10 @@
let loading = $state(true);
let error: string | null = $state(null);
let activeRun: string | null = null;
// Tracked when the user picks a memory from the canvas. Selection only —
// no API call. Mutations are gated to explicit modifier keys (shift/alt)
// whose affordances are mirrored by the on-canvas hint line.
let selectedMemoryId: string | null = $state(null);
let stopReducedMotion: (() => void) | null = null;
onMount(() => {
@ -188,6 +192,25 @@
if (error) return [statusItem(`ERROR - ${error}`.slice(0, 72), SCARLET)];
if (memories.length === 0) return [statusItem('EMPTY MEMORY FIELD', MUTED)];
// Persistent on-canvas affordance: a plain click selects; shift and alt are
// the only modifiers that mutate. Anchored low so it never collides with the
// row band above, and shaped as a "memory-hint" kind so it is NEVER picked as
// a memory row by the text layer.
const hint: MemoryTextItem = {
id: 'memories:hint',
kind: 'memory-hint',
text: 'click: select · shift: suppress · alt: unsuppress',
x: -0.82,
y: -0.9,
size: 0.022,
color: MUTED,
depth: 0.7,
weight: 0.5,
startFrame: REVEAL_ANCHOR,
revealSpan: 24,
maxWidthEm: 50
};
const aspect = viewportAspect();
const portrait = aspect < 0.85;
@ -221,6 +244,7 @@
maxWidthEm: 30
};
return [
hint,
header,
...rows.map((memory, i) => {
const retrieval = clamp01(memory.retrievalStrength);
@ -249,27 +273,30 @@
const rows = memories.slice(0, ROW_LIMIT);
const top = 0.72;
const rowStep = 1.5 / Math.max(1, ROW_LIMIT - 1);
return rows.map((memory, i) => {
const retrieval = clamp01(memory.retrievalStrength);
const retention = clamp01(memory.retentionStrength);
return {
id: `mem:${memory.id}`,
kind: 'memory',
memoryId: memory.id,
text: memoryLine(memory, false),
x: -0.88,
y: top - i * rowStep,
size: 0.026,
color: CYAN,
depth: Math.max(MIN_VISIBLE_DEPTH, retrieval),
weight: retention,
startFrame: REVEAL_ANCHOR + i * 2,
revealSpan: 20,
maxWidthEm: 46,
hitPadX: 0.03,
hitPadY: 0.015
};
});
return [
hint,
...rows.map((memory, i) => {
const retrieval = clamp01(memory.retrievalStrength);
const retention = clamp01(memory.retentionStrength);
return {
id: `mem:${memory.id}`,
kind: 'memory',
memoryId: memory.id,
text: memoryLine(memory, false),
x: -0.88,
y: top - i * rowStep,
size: 0.026,
color: CYAN,
depth: Math.max(MIN_VISIBLE_DEPTH, retrieval),
weight: retention,
startFrame: REVEAL_ANCHOR + i * 2,
revealSpan: 20,
maxWidthEm: 46,
hitPadX: 0.03,
hitPadY: 0.015
};
})
];
}
function pointerToNdc(e: PointerEvent | MouseEvent): { x: number; y: number } | null {
@ -321,39 +348,37 @@
if (hit?.kind !== 'memory') return;
const item = hit.payload as MemoryTextItem;
if (!item.memoryId) return;
// Immune actions surfaced right on the field (backend demoability):
// Read-intent click must SELECT only — no API call. Mutations are
// gated to explicit modifier keys whose affordances are mirrored by
// the on-canvas hint line ("shift: suppress · alt: unsuppress").
// plain click -> select (no mutation)
// shift-click -> suppress (macrophage engulfs; cell scars)
// alt-click -> unsuppress (labile release)
// plain click -> promote (retention boost)
try {
if (e.shiftKey) {
await api.memories.suppress(item.memoryId, 'suppressed from memories field');
suppressedIds = new Set(suppressedIds).add(item.memoryId);
} else if (e.altKey) {
// unsuppress COMPOUNDS down by one — a memory suppressed twice is still
// suppressed after one unsuppress. Only clear the scar when the backend
// says it's fully released (stillSuppressed=false), else the cell would
// lie (render healthy while retrieval is still penalized).
const res = await api.memories.unsuppress(item.memoryId);
if (!res.stillSuppressed) {
const next = new Set(suppressedIds);
next.delete(item.memoryId);
suppressedIds = next;
selectedMemoryId = item.memoryId;
if (e.shiftKey || e.altKey) {
try {
if (e.shiftKey) {
await api.memories.suppress(item.memoryId, 'suppressed from memories field');
suppressedIds = new Set(suppressedIds).add(item.memoryId);
} else if (e.altKey) {
// unsuppress COMPOUNDS down by one — a memory suppressed twice is still
// suppressed after one unsuppress. Only clear the scar when the backend
// says it's fully released (stillSuppressed=false), else the cell would
// lie (render healthy while retrieval is still penalized).
const res = await api.memories.unsuppress(item.memoryId);
if (!res.stillSuppressed) {
const next = new Set(suppressedIds);
next.delete(item.memoryId);
suppressedIds = next;
}
}
} else {
const promoted = await api.memories.promote(item.memoryId);
memories = memories.map((memory) =>
memory.id === promoted.id
? { ...memory, retentionStrength: promoted.retentionStrength }
: memory
);
error = null;
textPass.setText(buildTextItems());
fieldPass?.setCells(buildFieldCells());
} catch (err) {
error = err instanceof Error ? err.message : 'UNKNOWN MEMORY ACTION ERROR';
textPass.setText(buildTextItems());
}
error = null;
textPass.setText(buildTextItems());
fieldPass?.setCells(buildFieldCells());
} catch (err) {
error = err instanceof Error ? err.message : 'UNKNOWN MEMORY ACTION ERROR';
textPass.setText(buildTextItems());
}
}
</script>

View file

@ -26,6 +26,9 @@
let loading = $state(true);
let error: string | null = $state(null);
let consolidation: ConsolidationResult | null = $state(null);
// Tracked when the user picks a vital. Selection only — no API call.
// Consolidation runs only from the explicit [ CONSOLIDATE ] button on settings.
let selectedVitalId: string | null = $state(null);
const statsScene = $derived.by<RouteSceneModel>(() => {
const receipts = stats ? buildReceipts(stats, consolidation) : [];
@ -58,17 +61,13 @@
}
}
async function handleRoutePick(pick: RoutePick) {
if (pick.kind !== 'stats-vital') return;
loading = true;
error = null;
try {
consolidation = await api.consolidate();
stats = await api.stats();
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
loading = false;
function handleRoutePick(pick: RoutePick) {
// Plain click on a vital must SELECT/INSPECT only — never mutate.
// Consolidation is an expensive real FSRS mutation and lives behind the
// EXPLICIT [ CONSOLIDATE ] action button on the settings page. Reading a
// number here must not silently rewrite the whole memory system.
if (pick.kind === 'stats-vital') {
selectedVitalId = pick.id;
}
}

View file

@ -18,8 +18,9 @@
import AmbientAwarenessStrip from '$lib/components/AmbientAwarenessStrip.svelte';
import VerdictBar from '$lib/components/VerdictBar.svelte';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
import Icon, { type IconName } from '$lib/components/Icon.svelte';
import Icon from '$lib/components/Icon.svelte';
import { initTheme } from '$stores/theme';
import { OS_ROUTES, DOCK_ROUTES, routesByGroup, HOME_ROUTE } from '$lib/os-routes';
let { children } = $props();
let showCommandPalette = $state(false);
@ -29,12 +30,19 @@
$page.url.pathname.startsWith(base) ? $page.url.pathname.slice(base.length) || '/' : $page.url.pathname
);
let isMarketingRoute = $derived(dashboardPath === '/waitlist' || dashboardPath.startsWith('/waitlist/'));
// Zero-DOM Cognitive OS (Sam, Jul 9 2026): EVERY organ is a full-bleed
// all-WebGPU surface. DOM app chrome (sidebar nav, websocket toasts,
// ambient orbs, command palette) is bypassed for all dashboard routes so
// each route is a single shareable canvas — nav itself is drawn in WebGPU.
// Only true marketing/waitlist pages keep DOM. Recordings stay clean.
let isImmersiveRoute = $derived(!isMarketingRoute);
// The organs are full-bleed WebGPU canvases. The OS shell (persistent dock +
// ⌘K palette) is NOT the old flex-sidebar — it FLOATS over the canvas as an
// overlay so it never fights the `fixed inset-0` organ layout. It shows on
// every dashboard route so no canvas page is a navigation island (the launch
// audit: 6 organs had no desktop nav, Palace had none at all).
//
// Recording cleanliness is preserved by ?capture=1 (or ?capture): the shell
// hides so hero footage / ?frame=N captures stay pure canvas.
let isCaptureMode = $derived(
$page.url.searchParams.has('capture') && $page.url.searchParams.get('capture') !== '0'
);
// Show the floating OS shell on real dashboard routes (not marketing, not capture).
let showShell = $derived(!isMarketingRoute && !isCaptureMode);
onMount(() => {
// Live nervous system: every immersive organ consumes the WebSocket
@ -50,7 +58,7 @@
const teardownTheme = initTheme();
function onKeyDown(e: KeyboardEvent) {
if (isMarketingRoute || isImmersiveRoute) return;
if (isMarketingRoute || isCaptureMode) return;
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
showCommandPalette = !showCommandPalette;
@ -65,21 +73,9 @@
return;
}
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.key === '/') {
e.preventDefault();
const searchInput = document.querySelector<HTMLInputElement>('input[type="text"]');
searchInput?.focus();
return;
}
// Single-key navigation shortcuts
const shortcutMap: Record<string, string> = {
g: '/graph', m: '/memories', t: '/timeline', f: '/feed',
e: '/explore', i: '/intentions', s: '/stats',
r: '/reasoning', a: '/activation', d: '/dreams',
c: '/schedule', p: '/importance', u: '/duplicates',
x: '/contradictions', n: '/patterns',
};
const target = shortcutMap[e.key.toLowerCase()];
// Single-key navigation shortcuts — derived from the canonical registry
// so there is ONE source of truth (no drift between dock/palette/keys).
const target = SHORTCUT_MAP[e.key.toLowerCase()];
if (target && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
goto(`${base}${target}`);
@ -107,45 +103,34 @@
});
});
// Each nav item carries a UNIQUE semantic icon (see Icon.svelte). The old
// set reused the same Unicode glyph across multiple items; every entry here
// now has a distinct silhouette that reads instantly.
const nav: { href: string; label: string; icon: IconName; shortcut: string }[] = [
{ href: '/observatory', label: 'Observatory', icon: 'sparkle', shortcut: 'O' },
{ href: '/blackbox', label: 'Black Box', icon: 'blackbox', shortcut: 'B' },
{ href: '/memory-prs', label: 'Memory PRs', icon: 'memorypr', shortcut: 'Q' },
{ href: '/graph', label: 'Graph', icon: 'graph', shortcut: 'G' },
{ href: '/reasoning', label: 'Reasoning', icon: 'reasoning', shortcut: 'R' },
{ href: '/memories', label: 'Memories', icon: 'memories', shortcut: 'M' },
{ href: '/timeline', label: 'Timeline', icon: 'timeline', shortcut: 'T' },
{ href: '/feed', label: 'Feed', icon: 'feed', shortcut: 'F' },
{ href: '/explore', label: 'Explore', icon: 'explore', shortcut: 'E' },
{ href: '/activation', label: 'Activation', icon: 'activation', shortcut: 'A' },
{ href: '/dreams', label: 'Dreams', icon: 'dreams', shortcut: 'D' },
{ href: '/schedule', label: 'Schedule', icon: 'schedule', shortcut: 'C' },
{ href: '/importance', label: 'Importance', icon: 'importance', shortcut: 'P' },
{ href: '/duplicates', label: 'Duplicates', icon: 'duplicates', shortcut: 'U' },
{ href: '/contradictions', label: 'Contradictions', icon: 'contradictions', shortcut: 'X' },
{ href: '/patterns', label: 'Patterns', icon: 'patterns', shortcut: 'N' },
{ href: '/intentions', label: 'Intentions', icon: 'intentions', shortcut: 'I' },
{ href: '/stats', label: 'Stats', icon: 'stats', shortcut: 'S' },
{ href: '/settings', label: 'Settings', icon: 'settings', shortcut: ',' },
];
// Mobile nav shows top 5 items
const mobileNav = nav.slice(0, 5);
// ── All nav surfaces derive from the ONE canonical registry (os-routes.ts) ──
// Single-key shortcut map, built from the registry (no hand-maintained drift).
const SHORTCUT_MAP: Record<string, string> = Object.fromEntries(
OS_ROUTES.filter((r) => r.shortcut).map((r) => [r.shortcut!.toLowerCase(), r.href])
);
function isActive(href: string, currentPath: string): boolean {
const path = currentPath.startsWith(base) ? currentPath.slice(base.length) || '/' : currentPath;
if (href === '/graph') return path === '/' || path === '/graph';
return path.startsWith(href);
if (href === HOME_ROUTE) return path === '/' || path === href;
return path === href || path.startsWith(href + '/');
}
let filteredNav = $derived(
cmdQuery
? nav.filter(n => n.label.toLowerCase().includes(cmdQuery.toLowerCase()))
: nav
// The command palette searches label + purpose across ALL 20 organs, grouped.
let paletteGroups = $derived(
routesByGroup()
.map((g) => ({
group: g.group,
routes: cmdQuery
? g.routes.filter(
(r) =>
r.label.toLowerCase().includes(cmdQuery.toLowerCase()) ||
r.purpose.toLowerCase().includes(cmdQuery.toLowerCase())
)
: g.routes
}))
.filter((g) => g.routes.length > 0)
);
let paletteFlat = $derived(paletteGroups.flatMap((g) => g.routes));
function cmdNavigate(href: string) {
showCommandPalette = false;
@ -154,161 +139,131 @@
}
</script>
{#if isMarketingRoute || isImmersiveRoute}
{@render children()}
{:else}
<!-- Ambient background orbs -->
<div class="ambient-orb ambient-orb-1" aria-hidden="true"></div>
<div class="ambient-orb ambient-orb-2" aria-hidden="true"></div>
<div class="ambient-orb ambient-orb-3" aria-hidden="true"></div>
<!-- Organs always render FULL-BLEED (they own the viewport as fixed-inset
WebGPU canvases). The OS shell floats OVER them, so it never fights the
canvas layout the way the old flex-sidebar did. -->
{@render children()}
<!-- Desktop: sidebar + content -->
<!-- Mobile: content + bottom nav -->
<div class="flex flex-col md:flex-row h-screen overflow-hidden bg-void relative z-[1]">
<!-- Desktop Sidebar (hidden on mobile) -->
<nav class="hidden md:flex w-16 lg:w-56 flex-shrink-0 glass-sidebar flex-col">
<!-- Logo -->
<a href="{base}/graph" class="logo-link flex items-center gap-3 px-4 py-5 border-b border-synapse/10">
<div class="logo-mark w-8 h-8 rounded-lg bg-gradient-to-br from-dream to-synapse flex items-center justify-center text-bright shadow-lg shadow-synapse/20">
<Icon name="logo" size={18} strokeWidth={1.8} />
</div>
<span class="hidden lg:block text-sm font-semibold text-bright tracking-[0.18em]">VESTIGE</span>
{#if showShell}
<!-- ── Persistent desktop dock (floating, left edge) ──────────────────────
Every route can reach every organ + ⌘K. No canvas page is an island. -->
<nav
class="os-dock hidden md:flex"
aria-label="VestigeOS navigation"
>
<a
href="{base}{HOME_ROUTE}"
class="os-dock-logo"
title="Palace — home"
aria-label="Palace, VestigeOS home"
>
<Icon name="logo" size={18} strokeWidth={1.8} />
</a>
<div class="os-dock-items">
{#each DOCK_ROUTES as item}
{@const active = isActive(item.href, $page.url.pathname)}
<a
href="{base}{item.href}"
class="os-dock-link {active ? 'os-dock-active' : ''}"
title="{item.label}{item.purpose}"
aria-current={active ? 'page' : undefined}
>
<span class="os-dock-icon"><Icon name={item.icon} size={20} /></span>
<span class="os-dock-label">{item.label}</span>
{#if item.shortcut}<span class="os-dock-key">{item.shortcut}</span>{/if}
</a>
{/each}
<!-- Command / More — the doorway to all 20 organs. -->
<button
class="os-dock-link os-dock-command"
onclick={() => { showCommandPalette = true; cmdQuery = ''; requestAnimationFrame(() => cmdInput?.focus()); }}
title="Command palette (⌘K) — jump to any organ"
>
<span class="os-dock-icon"><Icon name="command" size={20} /></span>
<span class="os-dock-label">Command</span>
<span class="os-dock-key">⌘K</span>
</button>
</div>
<div class="os-dock-footer">
<div class="os-dock-status" title={$isConnected ? 'Live' : 'Offline'}>
<span class="os-dot {$isConnected ? 'os-dot-live' : 'os-dot-off'}"></span>
<span class="os-dock-label os-dock-status-text">{$isConnected ? 'Live' : 'Offline'}</span>
</div>
<div class="os-dock-theme"><ThemeToggle /></div>
</div>
</nav>
<!-- ── Mobile bottom bar (primary organs + More→palette) ──────────────── -->
<nav class="os-mobilebar md:hidden safe-bottom" aria-label="VestigeOS navigation">
{#each DOCK_ROUTES.slice(0, 5) as item}
{@const active = isActive(item.href, $page.url.pathname)}
<a
href="{base}{item.href}"
class="os-mobile-link {active ? 'os-mobile-active' : ''}"
aria-current={active ? 'page' : undefined}
>
<Icon name={item.icon} size={20} />
<span class="os-mobile-label">{item.label}</span>
</a>
<!-- Nav items -->
<div class="flex-1 min-h-0 overflow-y-auto py-3 flex flex-col gap-1 px-2">
{#each nav as item}
{@const active = isActive(item.href, $page.url.pathname)}
<a
href="{base}{item.href}"
class="nav-link group flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm
{active
? 'bg-synapse/15 text-synapse-glow border border-synapse/30 shadow-[0_0_12px_rgba(99,102,241,0.15)] nav-active-border'
: 'text-dim hover:text-text hover:bg-white/[0.03] border border-transparent'}"
>
<span class="nav-icon w-5 flex justify-center transition-transform duration-200 group-hover:scale-110">
<Icon name={item.icon} size={18} />
</span>
<span class="hidden lg:block">{item.label}</span>
<span class="hidden lg:block ml-auto text-[10px] text-muted/50 font-mono">{item.shortcut}</span>
</a>
{/each}
</div>
<!-- Quick action -->
<div class="px-2 pb-2">
<button
onclick={() => { showCommandPalette = true; cmdQuery = ''; requestAnimationFrame(() => cmdInput?.focus()); }}
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-xs text-muted hover:text-dim hover:bg-white/[0.03] transition border border-subtle/15"
>
<Icon name="command" size={14} />
<span class="hidden lg:block">Command</span>
<span class="hidden lg:block ml-auto text-[10px] font-mono bg-white/[0.04] px-1.5 py-0.5 rounded">⌘K</span>
</button>
</div>
<!-- Status footer -->
<div class="px-3 py-4 border-t border-synapse/10 space-y-2">
<div class="flex items-center gap-2 text-xs">
<div class="w-2 h-2 rounded-full {$isConnected ? 'bg-recall animate-pulse-glow' : 'bg-decay'}"></div>
<span class="hidden lg:block text-dim">{$isConnected ? 'Connected' : 'Offline'}</span>
<div class="ml-auto">
<ThemeToggle />
</div>
</div>
<div class="hidden lg:block text-xs text-muted space-y-0.5">
<div>{$memoryCount} memories</div>
<div>{($avgRetention * 100).toFixed(0)}% retention</div>
<!-- v2.0.7: surface uptime_secs from the Heartbeat event. Fires
every 30s so this self-refreshes. "up 3d 4h" format. -->
{#if $uptimeSeconds > 0}
<div title="MCP server uptime">up {formatUptime($uptimeSeconds)}</div>
{/if}
</div>
{#if $suppressedCount > 0}
<div class="hidden lg:block pt-1">
<ForgettingIndicator />
</div>
{/if}
</div>
</nav>
<!-- Main content -->
<main class="flex-1 flex flex-col min-h-0 pb-16 md:pb-0">
<AmbientAwarenessStrip />
<VerdictBar />
<div class="flex-1 min-h-0 overflow-y-auto">
{@render children()}
</div>
</main>
<!-- Mobile Bottom Nav (hidden on desktop) -->
<nav class="md:hidden fixed bottom-0 inset-x-0 glass border-t border-synapse/10 z-40 safe-bottom">
<div class="flex items-center justify-around px-2 py-1">
{#each mobileNav as item}
{@const active = isActive(item.href, $page.url.pathname)}
<a
href="{base}{item.href}"
class="flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-all min-w-[3.5rem]
{active ? 'text-synapse-glow' : 'text-muted'}"
>
<Icon name={item.icon} size={20} />
<span class="text-[9px]">{item.label}</span>
</a>
{/each}
<!-- More button opens command palette on mobile -->
<button
onclick={() => { showCommandPalette = true; cmdQuery = ''; requestAnimationFrame(() => cmdInput?.focus()); }}
class="flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg text-muted min-w-[3.5rem]"
>
<span class="text-lg"></span>
<span class="text-[9px]">More</span>
</button>
</div>
</nav>
</div>
{/each}
<button
class="os-mobile-link"
onclick={() => { showCommandPalette = true; cmdQuery = ''; requestAnimationFrame(() => cmdInput?.focus()); }}
aria-label="More organs"
>
<Icon name="command" size={20} />
<span class="os-mobile-label">More</span>
</button>
</nav>
<!-- v2.2 Pulse — InsightToast overlay (floating, fixed) -->
<InsightToast />
{/if}
<!-- Command Palette overlay -->
{#if showCommandPalette && !isMarketingRoute && !isImmersiveRoute}
<!-- ── Command palette — ALL 20 organs, grouped, searchable, everywhere ──── -->
{#if showCommandPalette && showShell}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-start justify-center pt-[10vh] md:pt-[15vh] px-4 bg-void/60 backdrop-blur-sm"
class="fixed inset-0 z-[100] flex items-start justify-center pt-[10vh] md:pt-[14vh] px-4 bg-void/70 backdrop-blur-md"
onkeydown={(e) => { if (e.key === 'Escape') showCommandPalette = false; }}
onclick={(e) => { if (e.target === e.currentTarget) showCommandPalette = false; }}
>
<div class="w-full max-w-lg glass-panel rounded-xl shadow-2xl shadow-synapse/10 overflow-hidden">
<div class="w-full max-w-xl glass-panel rounded-xl shadow-2xl shadow-synapse/10 overflow-hidden">
<div class="flex items-center gap-3 px-4 py-3 border-b border-synapse/10">
<span class="text-synapse"><Icon name="search" size={16} /></span>
<input
bind:this={cmdInput}
bind:value={cmdQuery}
type="text"
placeholder="Navigate to..."
placeholder="Jump to any organ…"
class="flex-1 bg-transparent text-text text-sm placeholder:text-muted focus:outline-none"
onkeydown={(e) => {
if (e.key === 'Enter' && filteredNav.length > 0) {
cmdNavigate(filteredNav[0].href);
}
if (e.key === 'Enter' && paletteFlat.length > 0) cmdNavigate(paletteFlat[0].href);
}}
/>
<span class="text-[10px] text-muted font-mono bg-white/[0.04] px-1.5 py-0.5 rounded">esc</span>
</div>
<div class="max-h-72 overflow-y-auto py-1">
{#each filteredNav as item}
<button
onclick={() => cmdNavigate(item.href)}
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-dim hover:text-text hover:bg-white/[0.04] transition"
>
<span class="w-5 flex justify-center"><Icon name={item.icon} size={17} /></span>
<span>{item.label}</span>
<span class="ml-auto text-[10px] text-muted/50 font-mono hidden md:block">{item.shortcut}</span>
</button>
<div class="max-h-[60vh] overflow-y-auto py-1">
{#each paletteGroups as grp}
<div class="px-4 pt-3 pb-1 text-[10px] uppercase tracking-[0.16em] text-muted/70 font-mono">{grp.group}</div>
{#each grp.routes as item}
<button
onclick={() => cmdNavigate(item.href)}
class="w-full flex items-center gap-3 px-4 py-2 text-left text-sm text-dim hover:text-text hover:bg-white/[0.05] transition"
>
<span class="w-5 flex justify-center text-synapse/80"><Icon name={item.icon} size={17} /></span>
<span class="flex-1 min-w-0">
<span class="block">{item.label}</span>
<span class="block text-[11px] text-muted/60 truncate">{item.purpose}</span>
</span>
{#if item.shortcut}<span class="ml-auto text-[10px] text-muted/50 font-mono hidden md:block">{item.shortcut}</span>{/if}
</button>
{/each}
{/each}
{#if filteredNav.length === 0}
{#if paletteFlat.length === 0}
<div class="px-4 py-6 text-center text-sm text-muted">No matches</div>
{/if}
</div>
@ -321,24 +276,206 @@
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Logo breathes a faint synapse glow on hover — the mark feels live. */
.logo-mark {
transition:
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.3s ease;
/* ── Floating desktop dock ─────────────────────────────────────────────
Overlays the full-bleed canvas at the left edge. Collapsed to icons by
default; expands to labels on hover so it never steals canvas real
estate but stays one glance away. */
.os-dock {
position: fixed;
top: 50%;
left: 0.75rem;
transform: translateY(-50%);
z-index: 60;
flex-direction: column;
align-items: stretch;
gap: 0.35rem;
max-height: calc(100dvh - 1.5rem);
padding: 0.5rem 0.4rem;
border-radius: 1rem;
border: 1px solid rgba(129, 140, 248, 0.16);
background: rgba(6, 8, 14, 0.72);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.55);
width: 3.4rem;
transition: width 0.18s ease;
overflow: hidden;
}
.logo-link:hover .logo-mark {
transform: rotate(-6deg) scale(1.08);
box-shadow:
0 0 0 1px rgba(129, 140, 248, 0.4),
0 0 22px rgba(99, 102, 241, 0.5);
.os-dock:hover,
.os-dock:focus-within {
width: 13.5rem;
}
.os-dock-logo {
display: grid;
place-items: center;
width: 2.6rem;
height: 2.6rem;
margin: 0 auto 0.35rem;
border-radius: 0.7rem;
background: linear-gradient(135deg, var(--dream, #818cf8), var(--synapse, #6366f1));
color: #fff;
flex-shrink: 0;
box-shadow: 0 0 18px rgba(99, 102, 241, 0.35);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.os-dock-logo:hover {
transform: rotate(-6deg) scale(1.06);
}
.os-dock-items {
display: flex;
flex-direction: column;
gap: 0.15rem;
overflow-y: auto;
min-height: 0;
flex: 1;
}
.os-dock-link {
display: flex;
align-items: center;
gap: 0.7rem;
width: 100%;
padding: 0.55rem 0.6rem;
border-radius: 0.6rem;
color: #9aa7c2;
font-size: 0.85rem;
white-space: nowrap;
border: 1px solid transparent;
background: none;
cursor: pointer;
text-align: left;
}
.os-dock-link:hover {
color: #e8ecf1;
background: rgba(255, 255, 255, 0.04);
}
.os-dock-active {
color: #a5b4fc;
background: rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.3);
}
.os-dock-icon {
width: 1.4rem;
display: grid;
place-items: center;
flex-shrink: 0;
}
.os-dock-active .os-dock-icon :global(svg) {
filter: drop-shadow(0 0 6px rgba(129, 140, 248, 0.6));
}
.os-dock-label {
flex: 1;
opacity: 0;
transition: opacity 0.12s ease;
}
.os-dock:hover .os-dock-label,
.os-dock:focus-within .os-dock-label {
opacity: 1;
}
.os-dock-key {
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-size: 0.65rem;
color: rgba(154, 167, 194, 0.5);
opacity: 0;
}
.os-dock:hover .os-dock-key,
.os-dock:focus-within .os-dock-key {
opacity: 1;
}
.os-dock-command {
margin-top: 0.25rem;
border-top: 1px solid rgba(129, 140, 248, 0.1);
border-radius: 0.6rem;
color: #7c8aa8;
}
.os-dock-footer {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem 0.1rem;
margin-top: 0.3rem;
border-top: 1px solid rgba(129, 140, 248, 0.1);
}
.os-dock-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.72rem;
color: #7c8aa8;
}
.os-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 999px;
flex-shrink: 0;
}
.os-dot-live {
background: #29f2a9;
box-shadow: 0 0 8px #29f2a9;
}
.os-dot-off {
background: #ff6b6b;
}
.os-dock-status-text {
opacity: 0;
}
.os-dock:hover .os-dock-status-text,
.os-dock:focus-within .os-dock-status-text {
opacity: 1;
}
.os-dock-theme {
margin-left: auto;
opacity: 0;
}
.os-dock:hover .os-dock-theme,
.os-dock:focus-within .os-dock-theme {
opacity: 1;
}
/* The active nav item's icon picks up a soft drop-shadow glow so the
current location reads at a glance even in the collapsed (icon-only)
sidebar. */
.nav-link.text-synapse-glow .nav-icon :global(svg),
.nav-active-border .nav-icon :global(svg) {
filter: drop-shadow(0 0 6px rgba(129, 140, 248, 0.55));
/* ── Mobile bottom bar ───────────────────────────────────────────────── */
.os-mobilebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 60;
display: flex;
align-items: center;
justify-content: space-around;
padding: 0.3rem 0.4rem;
border-top: 1px solid rgba(129, 140, 248, 0.14);
background: rgba(6, 8, 14, 0.9);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.os-mobile-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
min-width: 3.2rem;
min-height: 2.75rem;
padding: 0.35rem 0.5rem;
border-radius: 0.6rem;
color: #7c8aa8;
background: none;
border: none;
cursor: pointer;
}
.os-mobile-active {
color: #a5b4fc;
}
.os-mobile-label {
font-size: 0.6rem;
}
@media (prefers-reduced-motion: reduce) {
.os-dock,
.os-dock-label,
.os-dock-key,
.os-dock-logo,
.os-dock-theme,
.os-dock-status-text {
transition: none;
}
}
</style>