Aggregates active agents, pending gates, recent completions, and stale
projects from the existing agents, approvals, activity, and projects
APIs into a single shape consumed by AssistantHomeGreeting. The pure
`composeHomeStatus` helper drives the tests; the hook wires it to
react-query and degrades each slice to empty on fetch error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce the single source-of-truth data structure for the 8 Studio
workshops (diagrams, icons, themes, wallpapers, documents, brand-kits,
social, convert) and a pure keyword-based intent classifier used by
StudioPromptBar to route freeform prompts to the right workshop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three phase plans for Wave 2 of the Nexus layout overhaul. Each plan
is self-contained with ownership boundaries, scope fence, file inventory,
implementation notes, acceptance criteria, and commit scheme — designed
for parallel subagent dispatch per MIGRATION-PLAN.md section 11.
Phase 9 (Assistant mode):
- Full-bleed chat at /assistant, no inner conversation list
- History slide-over (left) + Memory slide-over (right)
- Conversational home-state greeting replaces Dashboard
- ActionStrip with Promote/Attach/Memory/History
- New components: AssistantInputBar, ActionStrip, HistorySheet,
MemorySheet, AssistantHomeGreeting + useAssistantHomeStatus hook
- Owns: PersonalAssistant.tsx + components/assistant/**
Phase 10 (Studio mode):
- 8-card workshop grid replaces 7-tab ContentStudio
- ConvertPage folds in as 8th workshop (legacy /convert route kept)
- StudioPromptBar freeform input with keyword-based classifier
- Two-column workshop detail view (params left, preview right)
- Owns: ContentStudio.tsx + pages/StudioWorkshopDetail.tsx
+ components/studio/**
Phase 11 (Projects + Builder mode):
- ProjectCard with 72px Inter Black volt hero percentage
- 7-tab BuilderTabStrip (Overview/Issues/Agents/Gates/Costs/Activity/Org)
- Approvals -> Gates display-only rename
- OverviewTab with milestone checklist, origin chat card, activity
- Thin per-project wrappers reuse existing list components scoped
by projectId (escalate if not supported)
- useGateIndicator hook for the future Assistant dot notification
- Owns: Projects.tsx + ProjectDetail.tsx + components/projects/**
Ownership boundaries prevent parallel-dispatch file conflicts:
- App.tsx routing changes are controller-owned (post-Wave wiring)
- Each phase declares its file ownership and must not cross into others
- Existing list components reused as-is; escalate if not scope-compatible
- IconRail dot wiring for phase 11's gate indicator deferred to
post-Wave controller step
Dispatch pattern: three general-purpose subagents dispatched in parallel,
each handed the full plan text + the spec + ownership rules. Each
subagent implements its phase end-to-end with atomic commits per
logical unit. Controller reviews outputs after all three complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three cleanups from the Task 6 code-quality review:
1. Delete dead instanceSettingsTarget state and supporting code.
The useState, the readRememberedInstanceSettingsPath helper, the
INSTANCE_SETTINGS_MEMORY_KEY constant, and the localStorage-writing
effect were all residue from the old footer sidebar buttons that
Task 6 deleted. Verified via grep that no other file in ui/src
reads paperclip.lastInstanceSettingsPath.
2. Consolidate two near-identical mobile-nav-visibility effects into
one. The standalone isMobile-reset effect was a strict subset of
the scroll-listener effect's early-return branch.
3. Drop onToggleSidebar / onTogglePanel stub callbacks.
useKeyboardShortcuts declares these as optional and uses optional
chaining internally; the stubs were "type contract satisfaction"
the contract did not actually require.
No behavioral change. 38 frame tests still pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites Layout.tsx to compose the new Phase 8 frame (IconRail +
TopStrip) and remove the old chrome elements specified as killed in
docs/specs/2026-04-11-nexus-layout-overhaul.md §2:
Removed from chrome:
- 280px collapsible Sidebar / InstanceSidebar
- ChatPanel global slide-in right rail
- PropertiesPanel global slide-in right rail
- BreadcrumbBar (replaced by ModeBreadcrumb inside TopStrip)
- Footer row with Docs link, version tooltip, instance settings button,
chat toggle button, theme toggle button
- Effect that closed PropertiesPanel when chat opened
- Mobile sidebar drawer block
- Mobile sidebar swipe gesture listener
Preserved:
- Company-prefix URL sync and fallback redirect machinery
- First-run onboarding trigger
- WorktreeBanner, DevRestartBanner
- Scroll-based mobile nav visibility tracking
- Body overflow management
- Instance settings path memory
- Dialog overlays (NewIssue, NewProject, NewGoal, NewAgent)
- ToastViewport, CommandPalette
- MobileBottomNav (mobile only; Phase 15 replaces)
Added:
- IconRail mount with derived companyPrefix from matchedCompany or
selectedCompany
- TopStrip mount above the main content area
- hasUnknownCompanyPrefix fallback defaults to /assistant instead of
/dashboard (Dashboard is killed in the new IA)
- useKeyboardShortcuts.onSearch dispatches the same synthetic Meta+K
keydown as the CmdKButton shim
The Sidebar, InstanceSidebar, BreadcrumbBar, ChatPanel, PropertiesPanel,
ThemeContext, and useChatPanel files remain in the repo; Phase 16
deletes dead files after the other Phase 8 tasks are proven stable.
Pages render unchanged in the new frame and will look visually wrong
until Phases 9-13 rebuild their internals. That is the expected
intermediate state per the spec.
Part of Phase 8 of the Nexus layout overhaul (task 6 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
48px sticky top strip per docs/specs/2026-04-11-nexus-layout-overhaul.md
§4.2. Composes ModeBreadcrumb (left) + CmdKButton and GlobalMicButton
(right) inside a <header aria-label="Top bar"> landmark.
Completes the frame component set for Phase 8. The next task (task 6)
rewrites Layout.tsx to mount IconRail + TopStrip as the new global
chrome and delete the old sidebar/ChatPanel/PropertiesPanel/
BreadcrumbBar combination.
Part of Phase 8 of the Nexus layout overhaul (task 5 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Visual-only mic button for the top strip per
docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2. Renders three
specified states (idle / listening / speaking) but Phase 8 only
wires the idle state functionally. Phase 14 will toggle the state
prop from the voice pipeline without changing this component's
signature.
Uses text-primary/bg-primary for volt (already migrated in phases
1-3) and literal #166534 / #a0a0a0 for forest and silver, which
MIGRATION-PLAN.md §3 proposes as new semantic tokens that have not
yet shipped.
Part of Phase 8 of the Nexus layout overhaul (task 4 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Top-strip button that renders the ⌘K glyph and opens the existing
CommandPalette by dispatching a synthetic Meta+K keydown on document,
which CommandPalette.tsx already listens for at its useEffect
(lines 42-51). This is explicitly a Phase 8 shim; Phase 14 of
docs/specs/2026-04-11-nexus-layout-overhaul.md replaces it with a
proper command-palette context when globalizing the palette's
search index.
Part of Phase 8 of the Nexus layout overhaul (task 3 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code-quality review for Task 2 caught a startsWith false-positive:
`/instance/settings-foo` was matching the settings branch of
deriveBreadcrumbSegments and producing nonsense output like
["SETTINGS", "-FOO"]. Tightened the guard to require an exact
match or a literal trailing slash before entering the settings
branch.
Added three test cases to lock it in:
- /instance/settings (exact, no trailing) -> ["SETTINGS"]
- /instance/settings-foo -> ["HOME"]
- /instance/settings-foo/bar -> ["HOME"]
22 tests pass. No behavior change for any previously-tested path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uppercase slash-separated breadcrumb that derives from the current
pathname per docs/specs/2026-04-11-nexus-layout-overhaul.md §4.2.
Leaf segment in text-primary (volt), non-leaf segments in
text-muted-foreground (silver). Pure function deriveBreadcrumbSegments
is exported for unit testing and covers 16 route patterns plus the
catch-all HOME fallback.
The derivation intentionally collapses Phase 11's soon-to-be-demoted
routes (issues/agents/routines/goals/approvals/costs/activity/inbox/
execution-workspaces) under the PROJECTS umbrella segment. When
Phase 11 lands and those routes become /projects/:slug/<tab>, the
derivation will naturally produce PROJECTS / PROJECT-SLUG without
code changes.
Part of Phase 8 of the Nexus layout overhaul (task 2 of 7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses two Important findings from the Task 1 code-quality review:
1. Swap literal #faff69 hex for the text-primary / bg-primary token
(4 sites). The --primary CSS variable is already wired to volt via
MIGRATION-PLAN.md phases 1-3, and 50+ existing files use the token.
The original Task 1 commit landed on literal hex as a self-
documenting placeholder; moving to the token aligns with the rest
of the codebase and makes theme swaps a single-file change.
2. Add focus-visible styles to the Nexus mark link and each
DestinationLink. Previously the component relied on browser-default
outline, which on pure-black canvas + volt icons is both off-palette
and visually weak. Now uses a volt ring with background offset for
clear keyboard focus indication.
No behavior change. All 7 IconRail tests still pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spec compliance review for Task 1 flagged that the explicit
`export { MemoryRouter } from "react-router-dom"` added in 332ed47b
is redundant — the line immediately above already does `export *`,
which includes MemoryRouter. The explicit line was cleanup debt from
defensive "just in case" reasoning, not an actual fix.
No behavioral change. IconRail.test.tsx still resolves MemoryRouter
via the export * and all 7 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces the 56px left icon rail specified in
docs/specs/2026-04-11-nexus-layout-overhaul.md §4.1. Four primary
destinations (Assistant, Studio, Projects, Settings) rendered as Lucide
icons with silver default + volt active state and a 2px volt bar on the
right edge of the active item. Destinations are company-prefixed except
Settings, which points at the global /instance/settings/general route.
The Studio icon also highlights on /convert because Phase 10 folds
ConvertPage into Studio as a workshop. The Projects icon is the umbrella
for all Phase 11 per-project-tab routes (issues, agents, routines,
goals, approvals, costs, activity, inbox, execution-workspaces).
The rail is not yet mounted in Layout.tsx — that happens in task 6.
Part of the Nexus v1.7 structural overhaul (Phase 8 of MIGRATION-PLAN.md
§8b). Companion tests cover all 4 destinations, active-state derivation,
and aria-current semantics.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task-by-task implementation plan for Phase 8 of the Nexus layout
overhaul (docs/specs/2026-04-11-nexus-layout-overhaul.md §13).
Seven tasks:
1. IconRail component + test
2. ModeBreadcrumb component + test
3. CmdKButton shim component + test
4. GlobalMicButton scaffold + test
5. TopStrip composite + test
6. Layout.tsx rewrite (mount new frame, kill old chrome)
7. Manual smoke test
TDD throughout (write failing test, run, implement, run, commit).
One commit per task. Execution via subagent-driven-development with
fresh subagent per task and two-stage review.
The plan includes complete code for every task (no placeholders),
exact file paths, exact test patterns matching the existing vitest
manual-createRoot pattern from ChatInput.test.tsx, and wire-up
details for preserving Layout.tsx's non-chrome responsibilities
(company-prefix URL sync, first-run onboarding, body overflow,
instance settings memory, dialog overlays).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The usePiperTts hook imported a non-existent 'tts' namespace from
@mintplex-labs/piper-tts-web@1.0.4. The package exports named
functions (stored, download, predict, etc.) at the top level, not
under a tts namespace. The failing named-import threw at module-link
time, which crashed the lazy chunk for PersonalAssistant.tsx and
left /NEX/assistant blank with only a React error boundary fallback.
Two fixes in one file:
1. Import as namespace:
import * as tts from "@mintplex-labs/piper-tts-web"
ESM namespace imports synthesize a 'tts' object whose members are
the package's named exports, so the existing tts.stored() /
tts.download() / tts.predict() call sites bind to real functions
without touching the hook body.
2. Wrap predict() Blob result in URL.createObjectURL() before passing
to new Audio(). predict() returns Promise<Blob>, not a URL string,
and Audio() cannot accept a Blob directly. Added a shared cleanup
callback that revokes the object URL on onended/onerror and in the
catch path so we don't leak blob URLs on every speak invocation.
Bug 1 was the page-blanking crash at module load. Bug 2 was a latent
runtime crash behind the speak button click handler, surfaced while
the file was already being edited.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds docs/specs/2026-04-11-nexus-layout-overhaul.md defining the
complete layout and information architecture redesign:
- 4-destination primary nav (Assistant, Studio, Projects, Settings)
- 56px left icon rail + 48px top strip as global frame
- Assistant as default landing (full-bleed chat, no side panels)
- Studio as 8-card workshop grid (Convert folded in as 8th workshop)
- Projects with per-project Builder tabs replacing global routes
(Issues, Agents, Gates, Costs, Activity, Org become per-project;
Routines moves to Settings; Goals folds into Overview; Inbox
killed-replaced by assistant dot + cmdK)
- Promote-to-project transition (chat compresses to 30%, brainstormer
rises into 70%, inset shadow ripple, 700ms)
- Voice and Cmd-K as global affordances; ChatPanel/PropertiesPanel
killed as global rails
- Implementation phases 8-16 with parallel subagent dispatch pattern
Also promotes the previously untracked MIGRATION-PLAN.md into the
repo, and extends it with section 8b (structural overhaul summary
table linking to the spec) and section 11 (binding subagent dispatch
pattern for phases 8-16).
Approved 2026-04-11 in brainstorming session; all 25 decisions from
the discussion are captured in spec section 11.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to commit 91530b07 which only covered agents.adapter_config
.cwd. An audit found three additional user-facing endpoints that
accept filesystem paths without normalization. Same zero-terminal bug
in each: user supplies "~/foo", server stores it raw, downstream
consumers can't resolve the tilde.
Extract the two helpers (expandUserPath, normalizeWorkspaceDir) from
the agents.ts closure into a shared utility so all endpoints use the
same primitive.
new: server/src/utils/path-normalization.ts
- expandUserPath(candidate): resolves ~ / ~/foo to homedir() and
path.resolve() to absolute. Null-safe on non-string input.
- normalizeWorkspaceDir(rawPath, { field }): expand + assert
absolute + mkdir -p + stat isDirectory + log the change.
Throws unprocessable (422) on any filesystem failure with a
field-aware error message.
changed: server/src/routes/agents.ts
- Replaced the inline expandUserPath + normalizeAdapterConfigPaths
helpers with a narrow wrapper that delegates to the shared utility.
Three call sites (create, hire, patch) unchanged in behavior.
- Removed now-unused imports: mkdir, stat, homedir.
changed: server/src/routes/projects.ts
- POST /projects/:id/workspaces: normalize req.body.cwd before the
service call.
- PATCH /projects/:id/workspaces/:workspaceId: same.
- Added import.
changed: server/src/routes/execution-workspaces.ts
- PATCH /execution-workspaces/🆔 normalize req.body.cwd before the
patch object is built.
- Added import.
changed: server/src/services/nexus-settings.ts
- In set(): expand ~ in piperBinaryPath and whisperBinaryPath before
merging and validating. These are executable paths so we expand
but don't mkdir — caller still has to install the binary itself,
but the stored path is now resolvable by the server.
- Added import.
Not extended:
- Storage provider baseDir: computed at startup from environment,
not user request body. Sandboxed. No change needed.
- Instructions bundle paths: indirectly covered — the legacy path
resolver depends on cwd being absolute, which 91530b07 ensures.
- Chat file upload object keys: system-generated, not user-supplied.
Verification: npx tsc --noEmit on server — zero errors introduced in
any touched file. Dev server on :6100 still returns 200.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop showing Paperclip's board UI by default. First-time users now
land on Personal Assistant (v1.5), see a Nexus-first sidebar, and the
NexusOnboardingWizard (built in v1.5) actually fires on first run
instead of sitting behind a dead "Start Onboarding" button click.
App.tsx
- CompanyRootRedirect now reads useNexusMode() and lands the user
at /${prefix}/assistant by default. Only project_builder mode
lands at /${prefix}/dashboard. "personal_ai" and "both" (the
default) both go to the Assistant.
- NoCompaniesStartPage gutted: the old "Create your first company"
button is gone. Single-workspace mode doesn't ask users to name
workspaces; the onboarding wizard handles it. Replaced with a
minimal "Setting up your workspace..." loading shim.
- OnboardingRoutePage now auto-opens the wizard on mount when no
companies exist. Closes the dead-button gap: previously the user
had to click "Start Onboarding" to actually get the wizard; now
the wizard opens itself as soon as they land.
Sidebar.tsx
- Restructured around two mode-gated sections:
* Always visible (Nexus essentials): Assistant, Content Studio,
Convert, Inbox, Skills, Settings. Plus the New Issue button and
plugin sidebar items.
* project_builder-only: Work (Issues, Routines, Goals), Projects,
Agents, and the remaining Workspace items (Org, Costs, Activity).
- Top bar no longer renders a company switcher dropdown — single-
workspace mode shows the workspace name as a static label with
the search button beside it.
- Dashboard link removed from the always-visible section. The
default landing is /assistant; users who explicitly want the
Paperclip dashboard can type the URL or switch to project_builder
mode.
Layout.tsx
- Removed both <CompanyRail /> renderings (mobile and desktop
branches). Single-workspace mode doesn't need a multi-company
icon rail. Import preserved with a [nexus] comment for upstream
rebase compat.
- Onboarding useEffect's authenticated-mode gate removed (root
cause of the v1.5 wizard-not-firing bug on fresh DB). This
effect is now a belt-and-suspenders fallback; the real auto-
trigger lives in OnboardingRoutePage because Layout isn't
actually mounted during the zero-company first-run state
(CompanyRootRedirect navigates to /onboarding before Layout
ever renders).
NexusOnboardingWizard.tsx
- handleSubmit and handleStartChat both used to hardcode the post-
creation navigation to /${prefix}/dashboard. Now mode-aware:
project_builder lands at /dashboard, everything else lands at
/assistant. Matches the Sidebar and CompanyRootRedirect logic —
a fresh user never touches the Paperclip dashboard unless they
explicitly chose project_builder during the wizard.
Not changed:
- The Paperclip pages themselves (Dashboard, Issues, Projects,
Agents, Org, etc.) — still present, still accessible by URL,
still upstream-mergeable. Just hidden from the default nav.
- CompanyRail.tsx, CompanySwitcher.tsx, NewCompanyDialog — files
preserved for upstream rebase diff minimization. No call sites
remain.
- /NEX/companies route still registered in boardRoutes(), just
unlinked from the default UI.
TypeScript: zero new errors (pre-existing errors in AgentConfigForm,
command.tsx, useKeyboardShortcuts, usePiperTts, useVadRecorder,
OnboardingSummaryStep.test, PersonalAssistant unchanged).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zero-terminal bug fix. When a user set an agent's working directory
to "~/nexus-test-01" through the UI, the path was stored verbatim in
agents.adapter_config.cwd and the downstream local adapters (claude_
local, codex_local, gemini_local, cursor, opencode_local, hermes_
local, etc.) failed to resolve the tilde — shells expand ~, child
processes don't. The user was then expected to ssh in and create the
directory by hand, which contradicts Nexus's zero-terminal charter.
Add two helpers in server/src/routes/agents.ts:
expandUserPath(candidate)
Trim, expand leading "~" or "~/" to os.homedir(), then
path.resolve() to absolute form. Null-safe on non-string input.
normalizeAdapterConfigPaths(adapterConfig) [async]
If adapterConfig.cwd is a non-empty string, expand it, assert
absolute, mkdir -p (recursive), and stat to confirm it's a
directory. Any failure becomes an unprocessable (422) error with
the reason surfaced to the UI. Logs an info line when a path is
actually changed, so the audit trail records that Nexus expanded
a user-supplied tilde.
Wire into the three existing call sites:
POST /api/companies/:companyId/agent-hires
POST /api/companies/:companyId/agents
PATCH /api/agents/:id
...all of which previously called applyCreateDefaultsByAdapterType
then secretsSvc.normalizeAdapterConfigForPersistence. Added
normalizeAdapterConfigPaths between the secrets step and
assertAdapterConfigConstraints on create + hire, and between secrets
normalization and syncInstructionsBundleConfigFromFilePath on patch.
Each call site now stores a fully resolved absolute path and is
guaranteed the directory exists on disk.
DB state for the two agents that hit this bug today (Project Manager
and Engineer on the Nexus company) was already patched out-of-band
to /home/mikkel/nexus-test-01 by a direct SQL update and mkdir. This
commit prevents recurrence for any future agent-create or agent-patch.
Not addressed here (scope creep):
- instructionsFilePath / instructionsRootPath also accept
user-provided paths but are managed by a separate subsystem;
they may need their own tilde-expansion pass if the UI ever
exposes them directly.
- No restriction on where the cwd can be. Nexus runs as the host
user and trusts the caller. A future policy could limit cwd to
$HOME or a configured workspace root, but that's a separate
decision.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The company-aware router helper (applyCompanyPrefix / Link wrapper in
ui/src/lib/router.tsx) uses BOARD_ROUTE_ROOTS to tell the difference
between "this path is under /:companyPrefix/..." and "this is a raw
company prefix like /NEX." When a path segment isn't in BOARD_ROUTE_
ROOTS or GLOBAL_ROUTE_ROOTS, extractCompanyPrefixFromPath assumes
it's a company prefix and applyCompanyPrefix returns the path
unchanged instead of prepending the current company.
Every board route added in v1.5 (Personal Assistant) and v1.7
(Content Generation, Convert) was wired in App.tsx but never added
here. Sidebar's <SidebarNavItem to="/assistant"> therefore rendered
as /assistant (raw), the router treated "assistant" as a company
prefix, no company matched, and the user landed on the "Company not
found" 404 — this is how the /ASSISTANT/company/settings confusion
from the previous debug session was born: after the Layout
auto-recovery fix the broken link now redirects to /NEX/dashboard
but silently eats the Assistant navigation intent.
Added:
- assistant (v1.5 Personal Assistant)
- content-studio (v1.7 Content Generation)
- convert (v1.7 Format Conversion)
- plugins (board-scoped PluginPage at /:prefix/plugins/:id)
- tests (dev-only /tests/ux/runs RunTranscriptUxLab)
- settings (LegacySettingsRedirect at /:prefix/settings, which
then redirects to /instance/settings/general)
Also added a block comment above the set explaining the invariant
so future additions to App.tsx's boardRoutes() don't drift out of
sync again. Existing company-routes.test.ts passes unchanged
(2/2 green).
Nothing in the sidebar / nav wiring changed — the Nexus v1.5 and
v1.7 pages were always correctly routed in App.tsx. They were just
unreachable via Link clicks because the prefix classifier lied about
what counted as a board route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 VoiceCapability interface with whisperAvailable, piperAvailable, voiceTierSufficient
- Extend HardwareInfo with voiceCapability field
- Add detectVoiceCapability() probing whisper-cpp/whisper and piper with 2s timeout each
- voiceTierSufficient: true for apple_silicon/gpu, or cpu_only with >= 4GB free RAM
- Wrap voice probe in 3s timeout to avoid slowing hardware detection
- Route automatically includes voiceCapability via existing HardwareInfo return
- 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
- Export splitSentences() with title-abbreviation protection (Dr., Mr. etc.)
- Add synthesizeSentenceStream() AsyncGenerator yielding per-sentence audio chunks
- Add synthesizeMultiLang() synthesizing same text in N voices via Promise.all
- Add POST /api/synthesize/stream SSE endpoint with base64 audio per sentence
- Add POST /api/synthesize/multi-lang returning array of voiceId+audio pairs
- Existing POST /api/synthesize unchanged (backward compatible)