Second phase of the DESIGN.md migration. Rewrites the two color
dictionaries that back most status/priority/role indicators across the
app, and adds the controlled "chart palette" exception for agent role
colors per user decision in MIGRATION-PLAN section 9.
status-colors.ts (full rewrite, 109 -> 122 lines)
- All 5 dictionaries (statusBadge, statusDot, priorityColor,
issueStatusIcon, agentStatusDot) + 5 defaults rewritten to use
semantic token classes:
* done/completed/approved -> bg-success/15 text-success border-success/30
* error/failed/terminated/rejected -> bg-destructive/15 text-destructive border-destructive/30
* pending/paused/in_review -> bg-warning/15 text-warning border-warning/30
* running/in_progress -> bg-primary/15 text-primary border-primary/30
* idle/planned/backlog/todo -> bg-muted text-muted-foreground border-border
* blocked -> bg-destructive/10 text-destructive border-destructive/25 (softer)
* cancelled/archived -> bg-muted/40 text-muted-foreground/70 border-border
* priority urgent/high/medium/low -> destructive/warning/primary/muted
- Zero raw palette utilities remain (rg verified).
- All export identifiers and signatures preserved; 11 caller files
across ui/src compile unchanged.
- Note: timed_out was not explicitly mapped in the spec but exists
in the source; agent chose warning (semantically closer than error).
agent-role-colors.ts (full rewrite, 17 -> 68 lines)
- Controlled "chart palette" exception: 5 muted desaturated hues,
passed WCAG AA for all 10 combinations (dark + light), 7/10
also pass AAA.
- 11 AgentRole entries cycle through 5 slots via mod-5:
* slot 1 (volt #faff69 dark / olive #4f5100 light): general, pm
* slot 2 (teal #6ee7b7 / #0f766e): devops, cto
* slot 3 (lavender #c4b5fd / violet #6d28d9): designer, cmo
* slot 4 (amber #fcd34d / #b45309): ceo, cfo, researcher
* slot 5 (silver #a0a0a0 / gray #6b6b6b): engineer, qa
- Hue collisions past slot 5 are intentional and documented inline;
secondary differentiation relies on icons/labels.
index.css
- Added 5 --chart-role-* vars to :root and .dark (light + dark modes).
- Mirrored as --color-chart-role-N in @theme inline so
text-chart-role-1..5 become valid Tailwind utilities.
- Minimal surgical additions — nothing else touched.
Verification
- npx tsc --noEmit in ui/: zero errors in modified files. Pre-existing
errors in unrelated files (AgentConfigForm, command.tsx, etc.)
remain unchanged.
- rg '(bg|text|border|ring)-(red|blue|green|amber|yellow|cyan|violet|pink|slate|zinc|neutral|sky|teal|emerald|indigo|rose|orange)-\d'
on modified files: zero matches.
Test follow-up (out of scope, flagged for next PR)
- ui/src/lib/agent-role-colors.test.ts asserts each role has a
"dark:" prefix (no longer true — CSS vars handle dark variants)
and that all roles have unique colors (no longer true — 11 roles,
5 slots). Both assertions need rewriting.
Phase 3 follow-ups
- Sweep agent should verify no component layers raw palette
utilities on top of dictionary output.
- Consumers that previously wrapped statusBadge output in their own
border-* class may now double-border — worth a visual audit.
- agentStatusDot's animate-pulse modifier is gone except on
"running" — if any caller expected animation on "active",
inline handling needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First phase of the DESIGN.md (ClickHouse-inspired) migration. Rewrites
the foundation CSS variables and theme machinery; downstream phases
(status/role dictionaries, raw utility sweep) still pending.
index.css
- Full rewrite of @theme inline block. Dark (.dark) and light (:root)
token sets per MIGRATION-PLAN sections 3 and 5:
* Dark: pure black canvas (#000000), Neon Volt primary (#faff69),
Forest Green secondary (#166534), charcoal border (rgba(65,65,65,
0.8)), near-black cards (#141414), silver muted (#a0a0a0).
* Light: near-white canvas (#fafafa), Forest primary, Volt
downgraded to dark olive (#4f5100) for border/active use only,
silver inverted to #6b6b6b. Accessibility fallback, not brand.
- Added --warning (#f59e0b / #b45309), --success, and direct brand
token refs (--volt, --volt-pale, --volt-border, --forest, --near-
black, --hover-gray, --silver, --charcoal-border) exposed as
Tailwind utilities via --color-* mirrors.
- Added --destructive: #ef4444 (#dc2626 in light).
- Radius scale collapsed to 4px sharp / 8px comfortable / 9999px pill.
- Deleted .theme-tokyo-night.dark block entirely (was dead code —
ThemeContext never applied the class).
- Rewrote hljs syntax highlighting: one dark block under .dark .hljs
using volt for keywords, pale volt for strings, silver for
comments; one light block under .hljs using forest/dark-olive/
silver. Replaced all three Catppuccin + Tokyo Night hljs rule sets.
- Rewrote scrollbar rules to use var(--muted) / var(--charcoal-
border) / var(--hover-gray) instead of hardcoded oklch values.
- Added @font-face declarations for Inter (normal + italic) from the
self-hosted woff2 files at /fonts/InterVariable*.woff2. font-weight
100-900 range unlocks weight 900 for DESIGN.md hero moments from
a single variable font.
- Set --font-sans to Inter-first stack; body rule pulls the token.
ThemeContext.tsx
- Simplified to binary Theme = "light" | "dark". Dropped "custom"
theme type, PaletteRole interface, ROLE_TO_TOKEN map, and the
/api/nexus/settings custom-theme hydration effect.
- applyTheme() now just toggles .dark on <html> and sets
colorScheme. applyCustomTheme() left as a deprecated no-op (no
external callers but keeping the export avoids churn).
- Legacy localStorage values (catppuccin-mocha, tokyo-night, custom,
catppuccin-latte) coerced to "dark" on read so existing users
don't see a crash after the migration.
- Default theme: "dark".
Layout.tsx
- Dropped THEME_META import and the THEME_CYCLE map. Theme toggle
is now a binary sun/moon flip via toggleTheme().
index.html
- Added <link rel="preload" href="/fonts/InterVariable.woff2"
as="font" type="font/woff2" crossorigin>.
- Set inline style="background:#000000; color-scheme:dark;" on
<html> so the pre-React paint is already dark — no white flash.
- Boot script coerces legacy localStorage theme values and persists
"light" or "dark" only.
ui/public/fonts/
- Added InterVariable.woff2 (344 KB) and InterVariable-Italic.woff2
(379 KB), both Inter v4.x from rsms.me/inter (the canonical
upstream). Self-hosted for LAN/offline reliability.
Not changed:
- lib/status-colors.ts, lib/agent-role-colors.ts — next phase
- Any component files — phase 3
- MIGRATION-PLAN.md — will be updated with resolved decisions later
Expected visual state: pages using theme tokens (bg-background,
text-muted-foreground, border-border, ~1,250 instances) immediately
render with the new palette. Pages using raw Tailwind utilities
(bg-red-500, text-amber-600, ~274 instances) still show old colors
until phase 3 sweep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the URL contains a :companyPrefix segment that doesn't match any
fetched company, Layout previously rendered a dead-end NotFoundPage
with no recovery link. The sidebar's company switcher depends on
selectedCompany being non-null, so users landing on a bogus prefix
had no way back without hand-typing a new URL.
Replace the NotFoundPage branch with a redirect to the same path under
the first available company (selectedCompany ?? companies[0]). If the
rest of the path is empty, fall back to /dashboard so the target is
guaranteed to exist. The hasUnknownCompanyPrefix condition is already
gated on companies.length > 0, so the fallback is reachable; the old
NotFoundPage remains as a theoretical safety net if somehow both
selectedCompany and companies[0] are null.
Triggered by a user session after the embedded-postgres wipe: the
browser had a stale localStorage selectedCompanyId and the user
hand-typed URLs with guessed prefixes like /ASSISTANT/company/settings.
Hitting any invalid prefix stranded them on the 404 with no UI to
pick a different company.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zero-terminal first boot. Previously the bootstrap_ceo invite had to be
created via a CLI command (paperclipai auth bootstrap-ceo) and the UI
showed a code block instructing the user to run it. Nexus is meant to
be zero-terminal, so the server now auto-creates the invite on startup
when no instance admin exists and exposes its relative path through
/api/health. BootstrapPendingPage redirects straight to /invite/{token}.
The CLI command is left intact for headless/SSH-only setups.
Invite flow fixes that surfaced during testing:
- InviteLanding's invite query had default React Query refetch
behavior. After a successful bootstrap accept, the invite is marked
accepted server-side, so the refetch returned "not available" and
shadowed the success screen, making it look like the bootstrap had
failed when it actually succeeded. Set staleTime: Infinity +
refetchOnWindowFocus/Mount/Reconnect: false so the first fetch is a
one-shot snapshot.
- Reordered the render checks so result?.kind === "bootstrap" / "join"
are evaluated before the invite-availability error check — defensive
against any stray refetch that still leaks through.
- On bootstrap success, window.location.replace("/") lands the new
admin directly on the board; the "Bootstrap complete" confirmation
screen is now an unreachable safety net.
Vite onnxruntime middleware replaces the earlier public/ dump. The
previous commit put ort-wasm-simd-threaded.{mjs,wasm} in ui/public/ so
VAD's onnxWASMBasePath: "/" would find them. That works at runtime but
trips vite's dep optimizer: it scans onnxruntime-web, resolves the
dynamic import string to the public asset, and errors with "files in
/public should not be imported from source code." Remove the files and
add a vite plugin (configureServer middleware) that serves the two URLs
straight from node_modules/.pnpm/onnxruntime-web@*/. Runtime keeps
working and the files never enter vite's module graph.
Production build caveat: the middleware only runs in dev. When building
a static dist for production, the wasm files will need a different
mechanism (e.g. generateBundle hook). Not addressed here.
Also bundled (load-bearing for LAN browser testing):
- ui/src/lib/queryKeys.ts: add missing 'nexus' group. useNexusMode
referenced queryKeys.nexus.settings since commit 7bb72a5a (Phase
33-02) but the key was never added. Caused a blank screen crash on
any page that mounts Sidebar.
- ctl.sh: read PORT from .env instead of hardcoding 3100, and read it
once at the top so every subcommand honors it. Fixes the Version /
Mode showing '?' in status output after the port move to 6100.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HEAD had 3 pairs of drizzle-generated migrations colliding on indices 46-48
(chat set vs doc/feedback/routines set) with a journal that only referenced
one of each pair. Migrations 0047-0055 (chat_conversations, chat_messages,
bookmarks, chat_files, push_subscriptions, etc.) were committed as files on
disk but never added to _journal.json, so drizzle never applied them.
Rename the three non-chat ghost migrations out of the conflict zone
(0046/0047/0048 -> 0056/0057/0058) and extend the journal with entries for
all 12 previously-orphaned migrations so drizzle applies them in order on
fresh DB init.
Also mount chatRoutes() in app.ts — the router was defined in routes/chat.ts
but never wired up, so /api/companies/:id/{conversations,bookmarks} 404'd
even when tables existed.
Ship ort-wasm-simd-threaded.mjs + .wasm in ui/public so VAD can load the
onnxruntime module at /ort-wasm-simd-threaded.mjs instead of getting the
SPA HTML fallback.
Bundles pre-existing LAN-testing hunks in app.ts: conditional COOP/COEP
headers (only on secure/localhost origins) and Vite HMR host fix for
0.0.0.0 binding so the HMR client connects back to whatever hostname the
browser used. These are load-bearing for LAN browser testing on plain HTTP.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add streaming prop (default true) to ChatVoicePlayerProps
- Connect to POST /api/synthesize/stream via fetch + ReadableStream
- Parse SSE lines manually from response body stream
- First sentence audio begins playing as soon as first chunk arrives
- Subsequent sentences auto-play in sequence from audioQueue
- Show 'Sentence N of M' progress indicator during streaming playback
- Dot progress bar shows completed vs pending sentences
- Falls back to full-fetch mode on stream error or streaming=false
- Clean up all object URLs on unmount or new text
- BotFather numbered instructions (4-step setup guide)
- Token input with live validation via POST /api/telegram/token
- Success state showing connected bot username
- Error state with descriptive message
- Skip/Back/Next navigation; Next enabled only after validation
- postMessageAndStream data type extended with optional voiceMode field
- startStream signature updated: (userMessage, agentId?, voiceMode?)
- voiceMode forwarded into fetch body via postMessageAndStream call
- VoiceModeToggle: Text / Voice In / Full Voice pills with active/inactive styling
- Auto-play checkbox in full_voice mode, persists to nexus:voice:autoplay in localStorage
- useVoiceMode: reads/writes voiceMode via PATCH /api/nexus/settings with loading state
(deviation Rule 3: created missing blocking dependency for VoiceModeToggle)
- VoiceWaveform: 80x32 canvas with Web Audio AnalyserNode (fftSize=64), 20 animated bars drawn from frequency data using --primary color
- VoiceMicButton: three visual states — idle (Mic icon), recording (VoiceWaveform + ring-2 ring-primary), processing (Loader2 animate-spin)
- All three states have correct aria-labels per UI spec copywriting contract
- Add @ricky0123/vad-react dependency to ui/package.json
- Add copy-vad-assets npm script for reproducible asset copying
- Copy vad.worklet.bundle.min.js, silero_vad_legacy.onnx, silero_vad_v5.onnx to ui/public/
- Add COOP/COEP headers to Vite dev server config (SharedArrayBuffer support in dev)
- Update pnpm lockfile
- Add chatFileRoutes(db, storageService) after assistantHandoffRoutes (inside boardMutationGuard)
- Add nexusSettingsRoutes() after chatFileRoutes
- Extend nexusSettingsSchema with voiceEnabled: z.boolean().default(false)
- Update default return values in nexusSettingsService.get() to include voiceEnabled: false
- Add voiceEnabled?: boolean to NexusSettings client interface in hardware.ts
Replace streamEcho with Puter proxy AI call, inject memory facts as
system message, append memory after each turn. Assistant-to-PM handoff
creates new conversation with context summary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add OnboardingSummaryStep as step 5 of the wizard
- Add Skip buttons on step 1 (hardware) and step 2 (mode)
- Replace step 4 form submit with Review & finish -> step 5 flow
- Add Skip to summary on step 4
- Step indicator shows 'Summary' on step 5 instead of 'Step 5 of 4'
- Add deriveProviderLabel helper for provider display text
- Add handleStartChat that creates workspace then calls setChatOpen(true)
- Refactor shared workspace creation into createWorkspace() helper
- ProviderSelectionStep: three provider cards (Puter/Google/API key) with adapter badges
- Cards use border-primary bg-primary/5 when selected (matches ModeSelector pattern)
- PuterAuthButton/GoogleOAuthButton/ApiKeyEntryForm wired via callbacks
- NexusOnboardingWizard: step count 3→4, provider selection at step 3
- Parallel probe for hermes_local/claude_local/openclaw_gateway on wizard open
- Credentials stored after company creation (puterToken, googleOAuthStateId, apiKeyData)
- Skip always advances to step 4; Back from step 4 goes to step 3
- Refactor to 3-step flow: hardware detection, mode selection, root directory
- Add step indicator 'Step N of 3'
- Add HardwareSummaryStep on step 1 with dynamic heading
- Add ModeSelector on step 2 with 'both' pre-selected
- Add Back buttons on steps 2 and 3
- Persist selected mode via updateNexusSettings on wizard completion
- Reset step and mode on wizard close
28-02: ollamaApi client, model dropdown in config, skill badge
28-03: stateJson merge after heartbeat, HermesRuntimeCard in AgentOverview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ui/src/api/ollama.ts with ollamaApi.status() and ollamaApi.models()
- Replace free-text Model input with hybrid dropdown/fallback in HermesLocalConfigFields
- Dropdown shows pulled Ollama models with * prefix for recommended entries
- Install callout shown when Ollama is absent (with link to installUrl)
- Edit mode: selecting an Ollama model atomically sets model + provider:custom + base_url
- Manual entry fallback via "Other (manual entry)..." option or when Ollama absent
- Uses useCompany() hook for companyId (consistent with AgentConfigForm pattern)
1. Push notifications: call sendPushToAll after streaming completes
2. Mobile offline: add useOfflineQueue + banners to MobileChatView
3. New conversation streaming: call startStream in Path 1 handleSend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vendor-react manualChunks doesn't work with @vitejs/plugin-react JSX
runtime — react/react-dom stay in entry. Documented and removed.
PWA-02 and PWA-08 were implemented but not marked in REQUIREMENTS.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MobileChatView: full-screen mobile chat using 100dvh, back button, safe-area input
- ChatPanel: conditionally renders MobileChatView on mobile via useMediaQuery
- ChatConversationList: wraps ScrollArea in PullToRefresh for mobile
- ChatInput: pb-[env(safe-area-inset-bottom)] padding + 44px Send button touch target
- ChatConversationItem: min-h-[48px] touch target per UI-SPEC
- useMediaQuery: SSR-safe hook with addEventListener for live breakpoint updates
- usePullToRefresh: touch gesture hook with 64px threshold, haptic feedback via navigator.vibrate
- PullToRefresh: visual wrapper with Loader2 spinner, pull/release text indicators
- Add code category branch in ChatFilePreview routing to ChatCodeFilePreview
- Mark FILE-07 (one-click download) as Complete in REQUIREMENTS.md
- Mark FILE-13 (cross-device access) as Complete in REQUIREMENTS.md
- Update Traceability table for FILE-07 and FILE-13
- Add ChatCodeFilePreview component with hljs syntax highlighting
- Fetch file content from contentPath with credentials
- Use DOMParser-based safe rendering (no dangerouslySetInnerHTML)
- Include copy button, language label, and ChatFileCard download below
- Add extToLang extension-to-language mapping
- Register 14 common languages with hljs
- Add highlight.js as direct dependency in ui/package.json
- Add enableVoiceInput prop to ChatInput props interface
- Add handleTranscription callback that appends transcription text to textarea state
- Render VoiceRecordButton conditionally when enableVoiceInput is true
- Pass enableVoiceInput={true} from ChatPanel to ChatInput
- Mark INPUT-02, INPUT-03, INPUT-04 as Complete in REQUIREMENTS.md traceability table