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
46 changes: 26 additions & 20 deletions client/src/components/brain/constants.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { MessageSquare, Database, Calendar, Rss, Shield, Users, FolderKanban, Lightbulb, ClipboardList, Settings, Link2, BookOpen, Network, FileText, NotebookPen, Upload, Target, BookText, Music, Video } from 'lucide-react';
import { getPageNavTabs } from '../../../../server/lib/navManifest.js';
import { buildPageNavTabs } from '../../lib/pageNavTabs.js';

// Main navigation tabs.
// `fullBleed: true` marks a tab that fills the available height and owns its
// own internal scroll — Brain renders these inside an overflow-hidden wrapper
// with no padding (the rest scroll inside a padded wrapper). See issue #1177.
export const TABS = [
{ id: 'inbox', label: 'Inbox', icon: MessageSquare },
{ id: 'ideas', label: 'Ideas', icon: Lightbulb },
{ id: 'daily-log', label: 'Daily Log', icon: NotebookPen, fullBleed: true },
{ id: 'links', label: 'Links', icon: Link2 },
{ id: 'memory', label: 'Memory', icon: Database },
{ id: 'notes', label: 'Notes', icon: FileText, fullBleed: true },
{ id: 'graph', label: 'Graph', icon: Network, fullBleed: true },
{ id: 'digest', label: 'Digest', icon: Calendar },
{ id: 'feeds', label: 'Feeds', icon: Rss },
{ id: 'trust', label: 'Trust', icon: Shield },
{ id: 'import', label: 'Import', icon: Upload },
{ id: 'spotify', label: 'Spotify', icon: Music },
{ id: 'youtube', label: 'YouTube', icon: Video },
{ id: 'config', label: 'Config', icon: Settings }
];
// Icon + layout per tab id. The manifest (`tabGroup: 'brain'`) owns id/label/
// order — this file owns only how each tab looks. `fullBleed: true` marks a tab
// that fills the available height and owns its own internal scroll: Brain
// renders those inside an overflow-hidden wrapper with no padding (the rest
// scroll inside a padded wrapper). See issue #1177. Throws at import time on
// drift between the manifest and this map.
const TAB_PRESENTATION = {
inbox: { icon: MessageSquare },
ideas: { icon: Lightbulb },
'daily-log': { icon: NotebookPen, fullBleed: true },
links: { icon: Link2 },
memory: { icon: Database },
notes: { icon: FileText, fullBleed: true },
graph: { icon: Network, fullBleed: true },
digest: { icon: Calendar },
feeds: { icon: Rss },
trust: { icon: Shield },
import: { icon: Upload },
spotify: { icon: Music },
youtube: { icon: Video },
config: { icon: Settings },
};

export const TABS = buildPageNavTabs(getPageNavTabs('brain'), TAB_PRESENTATION, 'Brain');

// Tab ids that render full-bleed (derived from TABS so the list can't drift).
export const FULL_BLEED_TAB_IDS = new Set(TABS.filter((t) => t.fullBleed).map((t) => t.id));
Expand Down
22 changes: 21 additions & 1 deletion client/src/components/brain/constants.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { describe, expect, it } from 'vitest';
import { MEMORY_TABS, TABS } from './constants';
import { FULL_BLEED_TAB_IDS, MEMORY_TABS, TABS } from './constants';
import { expectPageNavTabs } from '../../test/pageNavTabAssertions.js';

describe('Brain navigation', () => {
it('keeps native Ideas on its dedicated URL-backed Brain tab', () => {
expect(TABS.map(({ id }) => id)).toContain('ideas');
expect(MEMORY_TABS.map(({ id }) => id)).not.toContain('ideas');
});
});

// Brain derives its tab bar from the nav manifest's `tabGroup: 'brain'` (#6383)
// — this pins the id/label/order the page means to render, and that every
// manifest tab has a presentation entry (icon, `fullBleed`) in constants.js,
// which would otherwise only surface as a thrown import-time error.
describe('Brain TABS ↔ nav manifest', () => {
it('renders the brain tabGroup in page order with a presentation entry each', () => {
expectPageNavTabs(TABS, [
'inbox:Inbox', 'ideas:Ideas', 'daily-log:Daily Log', 'links:Links',
'memory:Memory', 'notes:Notes', 'graph:Graph', 'digest:Digest',
'feeds:Feeds', 'trust:Trust', 'import:Import', 'spotify:Spotify',
'youtube:YouTube', 'config:Config',
]);
});

it('keeps the full-bleed set derived from the presentation map', () => {
expect([...FULL_BLEED_TAB_IDS].sort()).toEqual(['daily-log', 'graph', 'notes']);
});
});
42 changes: 24 additions & 18 deletions client/src/components/cos/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,31 @@ import {
import { normalizeReviewerSlug, REVIEWER_VALUES } from '../../lib/reviewerPins';
import { AVATAR_STYLE_LABELS } from '../../lib/avatarStyles';
import { inPlaceClipName } from '../../utils/animationClips';
import { getPageNavTabs } from '../../../../server/lib/navManifest.js';
import { buildPageNavTabs } from '../../lib/pageNavTabs.js';

// Icon per tab id. The manifest (`tabGroup: 'cos'`) owns id/label/order — this
// file owns only how each tab looks. Throws at import time on drift.
const TAB_PRESENTATION = {
briefing: { icon: Newspaper },
tasks: { icon: FileText },
agents: { icon: Cpu },
jobs: { icon: Bot },
runs: { icon: Play },
'run-events': { icon: ScrollText },
schedule: { icon: Clock },
workflow: { icon: ChartGantt },
digest: { icon: Calendar },
gsd: { icon: Compass },
productivity: { icon: BarChart2 },
learning: { icon: GraduationCap },
memory: { icon: Brain },
mind: { icon: MessageCircle },
health: { icon: Activity },
config: { icon: Settings },
};

export const TABS = [
{ id: 'briefing', label: 'Briefing', icon: Newspaper },
{ id: 'tasks', label: 'Tasks', icon: FileText },
{ id: 'agents', label: 'Agents', icon: Cpu },
{ id: 'jobs', label: 'System Tasks', icon: Bot },
{ id: 'runs', label: 'Runs', icon: Play },
{ id: 'run-events', label: 'Run Events', icon: ScrollText },
{ id: 'schedule', label: 'Schedule', icon: Clock },
{ id: 'workflow', label: 'Timeline', icon: ChartGantt },
{ id: 'digest', label: 'Digest', icon: Calendar },
{ id: 'gsd', label: 'GSD', icon: Compass },
{ id: 'productivity', label: 'Productivity', icon: BarChart2 },
{ id: 'learning', label: 'Learning', icon: GraduationCap },
{ id: 'memory', label: 'Memory', icon: Brain },
{ id: 'mind', label: 'Mind', icon: MessageCircle },
{ id: 'health', label: 'Health', icon: Activity },
{ id: 'config', label: 'Config', icon: Settings }
];
export const TABS = buildPageNavTabs(getPageNavTabs('cos'), TAB_PRESENTATION, 'CoS');

// Intentional category-color enum (#1909/#1924 caution), NOT off-token theme
// inconsistency: 9 files (CoSCharacter, CyberCoSAvatar, EsotericCoSAvatar,
Expand Down
19 changes: 18 additions & 1 deletion client/src/components/cos/constants.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
fresherHealth,
providerPinPatch,
hasProviderPin,
providerPinDivergesFromSchedule
providerPinDivergesFromSchedule,
TABS
} from './constants';
import { expectPageNavTabs } from '../../test/pageNavTabAssertions.js';

// These mirror the server's domainBudgets/domainAutonomy helpers so the UI's
// "is a cap set?" / "what mode?" view never disagrees with enforcement.
Expand Down Expand Up @@ -375,3 +377,18 @@ describe('REVIEWER_OPTIONS derivation', () => {
}
});
});

// CoS derives its tab bar from the nav manifest's `tabGroup: 'cos'` (#6383) —
// this pins the id/label/order the page means to render, and that every manifest
// tab has a presentation entry (icon) in constants.js, which would otherwise
// only surface as a thrown import-time error.
describe('CoS TABS ↔ nav manifest', () => {
it('renders the cos tabGroup in page order with a presentation entry each', () => {
expectPageNavTabs(TABS, [
'briefing:Briefing', 'tasks:Tasks', 'agents:Agents', 'jobs:System Tasks',
'runs:Runs', 'run-events:Run Events', 'schedule:Schedule', 'workflow:Timeline',
'digest:Digest', 'gsd:GSD', 'productivity:Productivity', 'learning:Learning',
'memory:Memory', 'mind:Mind', 'health:Health', 'config:Config',
]);
});
});
58 changes: 31 additions & 27 deletions client/src/components/digital-twin/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,43 @@ import {
Package,
UserRound
} from 'lucide-react';
import { getPageNavTabs } from '../../../../server/lib/navManifest.js';
import { buildPageNavTabs } from '../../lib/pageNavTabs.js';

// Main navigation sections, ordered by the group they belong to (see
// SECTION_GROUPS below). This array stays FLAT and stays the single registry of
// section ids: `server/lib/navManifest.test.js` scrapes `id:` out of it to prove
// every section is addressable as `/digital-twin/<id>` from ⌘K and voice, and
// its extractor stops at the first `];` — so a nested array here would silently
// truncate the guard. Grouping therefore lives in a separate constant.
export const TABS = [
// Icon per section id. The manifest (`tabGroup: 'digital-twin'`) owns
// id/label/order — this file owns only how each section looks, plus the
// SECTION_GROUPS slicing below. The page-local "Goals"/"Legacy" labels (vs the
// manifest's "Twin Goals"/"Legacy Bundle", which need the qualifier to be
// unambiguous in ⌘K) come from the manifest's `tabLabel`. Throws at import time
// on drift.
const TAB_PRESENTATION = {
// Profile
{ id: 'overview', label: 'Overview', icon: Heart },
{ id: 'identity', label: 'Identity', icon: Fingerprint },
{ id: 'personas', label: 'Personas', icon: Drama },
{ id: 'goals', label: 'Goals', icon: Target },
{ id: 'taste', label: 'Taste', icon: Palette },
overview: { icon: Heart },
identity: { icon: Fingerprint },
personas: { icon: Drama },
goals: { icon: Target },
taste: { icon: Palette },
// Sources
{ id: 'documents', label: 'Documents', icon: FileText },
{ id: 'import', label: 'Import', icon: Upload },
{ id: 'accounts', label: 'Accounts', icon: Globe },
{ id: 'interview', label: 'Interview', icon: MessageSquare },
{ id: 'autobiography', label: 'Autobiography', icon: PenLine },
{ id: 'enrich', label: 'Enrich', icon: Sparkles },
documents: { icon: FileText },
import: { icon: Upload },
accounts: { icon: Globe },
interview: { icon: MessageSquare },
autobiography: { icon: PenLine },
enrich: { icon: Sparkles },
// Assessment
{ id: 'test', label: 'Test', icon: CheckCircle },
{ id: 'personality', label: 'Personality', icon: Brain },
test: { icon: CheckCircle },
personality: { icon: Brain },
// Presence
{ id: 'voice', label: 'Voice', icon: Mic },
{ id: 'appearance', label: 'Appearance', icon: Camera },
{ id: 'avatar-bio', label: 'Avatar Bio', icon: UserRound },
voice: { icon: Mic },
appearance: { icon: Camera },
'avatar-bio': { icon: UserRound },
// Legacy
{ id: 'export', label: 'Export', icon: Download },
{ id: 'legacy', label: 'Legacy', icon: Package },
{ id: 'time-capsule', label: 'Time Capsule', icon: Archive }
];
export: { icon: Download },
legacy: { icon: Package },
'time-capsule': { icon: Archive },
};

export const TABS = buildPageNavTabs(getPageNavTabs('digital-twin'), TAB_PRESENTATION, 'Digital Twin');

// Two-level nav taxonomy (#3795). 19 sections in one flat strip stopped working
// as navigation, so they collapse into five groups keyed on what the user is
Expand Down
29 changes: 29 additions & 0 deletions client/src/components/digital-twin/constants.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { SECTION_GROUPS, TABS } from './constants';
import { expectPageNavTabs } from '../../test/pageNavTabAssertions.js';

// Digital Twin derives its section strip from the nav manifest's
// `tabGroup: 'digital-twin'` (#6383) — this pins the id/label/order the page
// means to render, and that every manifest section has a presentation entry
// (icon) in constants.js, which would otherwise only surface as a thrown
// import-time error. The short "Goals"/"Legacy" labels come from the manifest's
// `tabLabel`; ⌘K and voice still show the qualified "Twin Goals"/"Legacy Bundle".
describe('Digital Twin TABS ↔ nav manifest', () => {
it('renders the digital-twin tabGroup in page order with a presentation entry each', () => {
expectPageNavTabs(TABS, [
'overview:Overview', 'identity:Identity', 'personas:Personas', 'goals:Goals',
'taste:Taste', 'documents:Documents', 'import:Import', 'accounts:Accounts',
'interview:Interview', 'autobiography:Autobiography', 'enrich:Enrich',
'test:Test', 'personality:Personality', 'voice:Voice', 'appearance:Appearance',
'avatar-bio:Avatar Bio', 'export:Export', 'legacy:Legacy', 'time-capsule:Time Capsule',
]);
});

// The two-level nav (#3795) slices the SAME ids back out of the manifest
// order, so a section added to the tabGroup without a group lands nowhere.
it('assigns every section to exactly one SECTION_GROUPS group', () => {
const grouped = SECTION_GROUPS.flatMap((group) => group.sectionIds);
expect([...grouped].sort()).toEqual(TABS.map((tab) => tab.id).sort());
expect(new Set(grouped).size).toBe(grouped.length);
});
});
34 changes: 21 additions & 13 deletions client/src/components/meatspace/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,28 @@ import {
Stethoscope,
FileText,
} from 'lucide-react';
import { getPageNavTabs } from '../../../../server/lib/navManifest.js';
import { buildPageNavTabs } from '../../lib/pageNavTabs.js';

export const TABS = [
{ id: 'overview', label: 'Overview', icon: Activity },
{ id: 'age', label: 'Age', icon: Clock },
{ id: 'alcohol', label: 'Alcohol', icon: Beer },
{ id: 'blood', label: 'Blood', icon: HeartPulse },
{ id: 'body', label: 'Body', icon: Scale },
{ id: 'export', label: 'Export', icon: FileText },
{ id: 'genome', label: 'Genome', icon: Dna },
{ id: 'health', label: 'Health', icon: Stethoscope },
{ id: 'settings', label: 'Settings', icon: Settings },
{ id: 'lifestyle', label: 'Lifestyle', icon: ClipboardList },
{ id: 'nicotine', label: 'Nicotine', icon: Cigarette },
];
// Icon per tab id. The manifest (`tabGroup: 'meatspace'`) owns id/label/order —
// this file owns only how each tab looks; the page-local "Health" label (vs the
// manifest's "Body Health", which disambiguates it from CoS Health in ⌘K) comes
// from the manifest's `tabLabel`. Throws at import time on drift.
const TAB_PRESENTATION = {
overview: { icon: Activity },
age: { icon: Clock },
alcohol: { icon: Beer },
blood: { icon: HeartPulse },
body: { icon: Scale },
export: { icon: FileText },
genome: { icon: Dna },
health: { icon: Stethoscope },
settings: { icon: Settings },
lifestyle: { icon: ClipboardList },
nicotine: { icon: Cigarette },
};

export const TABS = buildPageNavTabs(getPageNavTabs('meatspace'), TAB_PRESENTATION, 'MeatSpace');

// Lifestyle adjustment table for death clock
export const LIFESTYLE_ADJUSTMENTS = {
Expand Down
19 changes: 19 additions & 0 deletions client/src/components/meatspace/constants.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it } from 'vitest';
import { TABS } from './constants';
import { expectPageNavTabs } from '../../test/pageNavTabAssertions.js';

// MeatSpace derives its tab bar from the nav manifest's `tabGroup: 'meatspace'`
// (#6383) — this pins the id/label/order the page means to render, and that
// every manifest tab has a presentation entry (icon) in constants.js, which
// would otherwise only surface as a thrown import-time error. The short "Health"
// label comes from the manifest's `tabLabel`; ⌘K and voice still show
// "Body Health" so it doesn't collide with CoS Health.
describe('MeatSpace TABS ↔ nav manifest', () => {
it('renders the meatspace tabGroup in page order with a presentation entry each', () => {
expectPageNavTabs(TABS, [
'overview:Overview', 'age:Age', 'alcohol:Alcohol', 'blood:Blood', 'body:Body',
'export:Export', 'genome:Genome', 'health:Health', 'settings:Settings',
'lifestyle:Lifestyle', 'nicotine:Nicotine',
]);
});
});
1 change: 1 addition & 0 deletions client/src/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ grep -i "what you want to do" client/src/lib/README.md
```

| `navFeatures.js` | `filterNavByFeatures(navEntries, isFeatureEnabled)` — drops nav-manifest entries whose optional instance feature (`post`, `datadog`, `jira`, `gsd`) is off. The single gate for BROWSE surfaces (sidebar, ⌘K); routes stay reachable by URL, bookmark, and voice. Pair with `useInstanceFeatures`. |
| `pageNavTabs.js` | `buildPageNavTabs(manifestTabs, presentation, pageName)` — merges `getPageNavTabs(group)` (nav-manifest, owns id/label/order) with a page-owned presentation map (icon, layout flags), throwing at import time when a manifest tab has no entry. The one way a tabbed page builds its tab bar. |
| `eidoverseFrame.js` | Versioned hosted Eidoverse message guards, exact section navigation allowlist, and browser label preferences. |
| `eidoverseWorldReset.js` | Client reset-reconciliation maps for Eidoverse source kinds and district asset slots; parity-tested against the authoritative server world-design contracts. |
| `postQuickSession.js` | Pure Quick POST duration presets, local-observation estimator, deterministic budget composer, and preview metadata. |
Expand Down
1 change: 1 addition & 0 deletions client/src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export * from './managedAppSources.js';
export * from './metronome.js';
export * from './mindThinkingPresets.js';
export * from './navFeatures.js';
export * from './pageNavTabs.js';
export * from './midiChords.js';
export * from './midiNotes.js';
export * from './midiPlayback.js';
Expand Down
28 changes: 28 additions & 0 deletions client/src/lib/pageNavTabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// The one way a page builds its own tab bar from the nav manifest.
//
// `server/lib/navManifest.js` is the single registry of navigable destinations,
// so a tabbed page must not restate its tab ids, labels or order in a local
// array — that second list drifts, and a tab that exists only there is
// unreachable from ⌘K and voice `ui_navigate`. Instead the page declares
// `tabGroup: '<group>'` on each manifest entry and pairs `getPageNavTabs(group)`
// with a PRESENTATION map holding only what the manifest has no business
// knowing: the icon, and any page-only layout flag (`fullBleed`, …).
//
// A manifest tab with no presentation entry throws HERE, at module load, rather
// than rendering an iconless tab or silently dropping it — a page that can't
// render its own nav is a build error, not a runtime degradation.

/**
* @param {Array<{id: string}>} manifestTabs from `getPageNavTabs(group)`
* @param {Record<string, object>} presentation per-tab-id icon/layout, page-owned
* @param {string} pageName used in the drift error, e.g. "Wiki"
*/
export const buildPageNavTabs = (manifestTabs, presentation, pageName) => (
manifestTabs.map((tab) => {
const tabPresentation = presentation[tab.id];
if (!tabPresentation) throw new Error(`${pageName}: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...tabPresentation };
})
);

export default buildPageNavTabs;
7 changes: 2 additions & 5 deletions client/src/pages/Calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TabPills from '../components/ui/TabPills';
import { useValidTab } from '../hooks/useValidTab';
import useUrlParams from '../hooks/useUrlParams';
import { getPageNavTabs } from '../../../server/lib/navManifest.js';
import { buildPageNavTabs } from '../lib/pageNavTabs.js';

import AgendaTab from '../components/calendar/AgendaTab';
import DayView from '../components/calendar/DayView';
Expand All @@ -33,11 +34,7 @@ const TAB_PRESENTATION = {
config: { icon: Settings },
};

export const TABS = getPageNavTabs('calendar').map((tab) => {
const presentation = TAB_PRESENTATION[tab.id];
if (!presentation) throw new Error(`Calendar: no tab presentation for manifest tab "${tab.id}"`);
return { ...tab, ...presentation };
});
export const TABS = buildPageNavTabs(getPageNavTabs('calendar'), TAB_PRESENTATION, 'Calendar');

export default function Calendar() {
const navigate = useNavigate();
Expand Down
Loading