36 KiB
07 — FRONTEND REBUILD GUIDE (Lovable)
What this is. The action-oriented, build-in-order playbook for rebuilding the Waggle OS web UI in Lovable against the existing, unchanged Fastify sidecar. The backend is the contract; the frontend is replaceable. Everything below is grounded in the backend map sections (
02c,03a–03g,04) and the liveapps/web/src/source. Field names, paths, ports, and tokens are quoted verbatim.Companion docs (read alongside):
sections/04-feature-map.md— full app↔endpoint matrix (the canonical screen list).sections/02c-shared-types-tiers.md— wire types, Zod request schemas, the 5-tier capability matrix.sections/03a-api-chat-agents.md— chat SSE event catalogue + approvals.sections/03f-api-realtime-ops.md— the 4 SSE streams + ops endpoints.sections/03g-api-cloud-billing-kvark.md— auth handshake, guards, Stripe.
0. The 60-second mental model
Waggle's UI is a single-page "desktop OS", not a multi-route app. React Router has exactly two routes
(/ → <Index>, * → <NotFound>). Index shows a boot screen then mounts <Desktop>, which is the
shell: it owns a window manager, a Dock, draggable app windows, and overlays. Apps are opened by appId,
not by URL. Everything talks to the backend through one singleton adapter pointed at the local Fastify
sidecar.
Lovable App
└─ ServiceProvider (calls adapter.connect() once, exposes useService())
└─ Desktop shell
├─ Dock (tier-filtered app launcher)
├─ WindowManager (open/close/focus windows by appId)
│ └─ AppWindow × N → renderAppContent(appId) → <XxxApp />
└─ Overlays (modals, rails, wizards)
── all of the above import the SAME `adapter` singleton ──
└─ adapter → http://127.0.0.1:3333 (Fastify sidecar)
Build the adapter + ServiceProvider + Desktop shell first. Everything else is screens that call adapter methods.
1. API base URL, transport, auth, headers, tier pattern
1.1 Base URL
| Concern | Value / behavior |
|---|---|
| Default server | http://127.0.0.1:3333 (constant DEFAULT_SERVER) |
| Override | localStorage["waggle:server-url"], settable via adapter.setServerUrl(url) |
| Resolution order | ctorArg ?? localStorage["waggle:server-url"] ?? DEFAULT_SERVER |
| Prefix | None. Every route hardcodes its own full path (/api/..., /health, /ws, /v1/...). Do not add a base prefix. |
| Same-origin reality | In the desktop binary the SPA is served BY the sidecar, so the page origin IS the API root (http://localhost:3333 or tauri://localhost). For a Lovable web rebuild, hit http://127.0.0.1:3333 explicitly; expect CORS to be pre-allowlisted for localhost:5173/8080/8081/8082/3333/1420 and tauri://localhost. |
1.2 Auth handshake (two-step, no login form)
There is no username/password UI for the local sidecar. Auth is a boot-generated session token:
- Static shell loads token-free — non-API GETs are auth-exempt.
- Fetch token once on connect:
GET /api/auth/session-token→{ token }. This route is auth-exempt but same-origin gated. Store it asauthToken. - Send Bearer on everything else: every
/api/*request must carryAuthorization: Bearer <token>. Missing →401 MISSING_TOKEN; wrong →401 INVALID_TOKEN. - WebSocket (optional) uses the token in the query string:
GET /ws?token=<authToken>(not a header). Wrong token closes the socket with code4001.
connect():
GET /health → probe (fallback to DEFAULT_SERVER once if stored URL fails; persist working URL)
GET /api/auth/session-token → { token }; this.authToken = token
set _connected = true
1.3 The single fetch wrapper (reproduce exactly)
Every request goes through one adapter.fetch(path, init). It MUST:
- Add
Content-Type: application/jsononly when a body is present and no content-type was supplied. (Body-less POSTs must NOT send a JSON content-type — this is a deliberate fix; sending it breaks some routes.) - Add
Authorization: Bearer <authToken>when the token is set. - On HTTP 403 with body
{ error: 'TIER_INSUFFICIENT' }, dispatch a global DOM eventwindow.dispatchEvent(new CustomEvent('waggle:tier-insufficient', { detail: { required, actual, message } })). This is the only trigger for the Upgrade modal. - Use a timeout: 10s default, 30s for upload/ingest/harvest. Throw
TimeoutError/NetworkError. - Handle rate limits: the sidecar enforces sliding-window limits (default 100/min;
/api/chat120/min;/api/vault/*/reveal5/min;/api/backup&/api/restore2/min;/api/browse/local/mkdir10/min). On429, readRetry-Afterand back off.
1.4 Read-side normalizers (reproduce or the UI crashes on undefined)
The backend and UI disagree on several field names. The adapter normalizes on read; you must too:
| Helper | Mapping |
|---|---|
unwrapArray<T>(data) |
accepts a raw array OR { results: [...] } / { <key>: [...] } envelope |
normalizeFrame(raw) |
frameType code I/F/E/D/T/N → insight/fact/event/decision/task/entity; importance string ↔ number 1–4 |
normalizeCronJob(raw) |
server cronExpr/lastRunAt/nextRunAt → client schedule/lastRun/nextRun |
getFleet() |
server durationMs/tokensUsed → client duration/tokenUsage |
getModelPricing() |
server inputPer1k/outputPer1k → client inputCostPer1k/outputCostPer1k |
getMemoryStats() |
server frameCount/entityCount/relationCount → client frames/entities/relations; tolerate workspace:null |
getModel() |
accepts raw string OR { model } |
Tauri dual-path (skip for Lovable web). A few memory methods (
addMemoryFrame,searchMemory,getKnowledgeGraph,getIdentity) branch onisTauri()and use Rust IPC. A web rebuild always takes theelseHTTP branch — ignore the IPC path entirely.
1.5 Tier gating pattern (read this before gating any feature)
- Fetch the user's tier from
GET /api/tier→{ tier, trialDaysRemaining?, trialExpired?, capabilities, usage }. - Always run the stored tier through
getEffectiveTier(tier, trialStartedAt)before reading capabilities — an expired TRIAL must collapse to FREE gating. (getEffectiveTier,getCapabilities,hasCapabilitylive in@waggle/shared/tiers; port them or reimplement.) - Gate UI on
TierCapabilitiesflags, not on the tier name.-1means unlimited (short-circuits to allowed). - The upgrade trigger is Skills + Connectors + Team features — never agents/memory (
spawnAgentsistruein every tier; memory/embedding quotas are-1everywhere). The full matrix is in02c §15.2. - Two distinct tier axes exist:
BillingTier='TRIAL'|'FREE'|'PRO'|'TEAMS'|'ENTERPRISE'(entitlements).UserTier='simple'|'professional'|'power'|'admin'(UI density / dock complexity).- The Dock is computed from both via
getDockForTier(userTier, billingTier).
2. Real-time: which endpoints stream, and how to consume each
There are five streaming surfaces. Four are SSE (EventSource), one is the chat POST-SSE hybrid, plus an
optional WebSocket. The chat stream is a POST, so you cannot use EventSource for it — you parse the
response body manually.
2.1 Chat token stream — POST /api/chat (SSE-formatted body)
-
Not a JSON endpoint and not
EventSource-compatible (EventSource is GET-only). Issue afetchPOST, readresponse.body.getReader(), decode, and split on\n\n; each frame isevent: <name>\ndata: <json>. -
All validation/auth happen before the server hijacks the reply. So a
400/403comes back as normal JSON; anything after is SSE. Checkresponse.ok/ content-type before entering the stream loop. -
Request body:
{ message (required), workspace?/workspaceId?, model?, session?, persona?, autonomy?, shape? }.autonomy = { level: 'normal'|'trusted'|'yolo', expiresAt? }. -
Event catalogue (handle ALL of these):
event:dataAction token{ content }append to streaming assistant text step{ content }render a progress line ("Recalling memories…", budget notes) tool{ name, input }show tool-call block (adapter normalizes name tool→tool_start)tool_result{ name, result, duration?, isError }close tool block (normalized tool_result→tool_end)file_created`{ filePath, fileAction: 'write' 'edit' approval_required{ requestId, toolName, input, sourceWorkspaceId, ...trustMeta }PAUSE. Render approve/deny; resolve via POST /api/approval/:requestIdgepa_choices{ original, expanded, clarifyingQuestions[], intent }offer ask-first clarification model_switch{ model, reason, primary }toast "switched to " notification{ type:'workflow_captured', title, message, pattern }suggest saving a workflow done{ content, usage{inputTokens,outputTokens}, toolsUsed[], model, cost?, tokens? }terminal success error{ message }terminal failure (user-friendly only) Tolerate two
doneusage shapes: agent-loop{ inputTokens, outputTokens }and echo/command-path{ prompt_tokens, completion_tokens, total_tokens }. -
Approvals are blocking. When
approval_requiredarrives the agent is paused awaiting a server-side Promise (auto-denies after 5 min). POST{ approved: boolean, always?: boolean, reason?, sourceWorkspaceId }to/api/approval/:requestId, echoingsourceWorkspaceIdverbatim.always:truepersists a grant.
2.2 The four GET SSE streams (EventSource)
| Endpoint | How to subscribe | Events you read |
|---|---|---|
GET /api/waggle/stream |
new EventSource(url) + addEventListener('signal', …) |
signal → full WaggleSignal JSON. Initial event: connected. Heartbeat : heartbeat every 30s. |
GET /api/events/stream |
addEventListener('audit', …) |
audit → full AuditEvent JSON. Initial data:{"type":"connected"}. |
GET /api/notifications/stream |
mixed: onmessage for unnamed frames + `addEventListener('subagent_status' |
'workflow_suggestion', …)` |
GET /api/harvest/progress |
new EventSource(url) |
{ phase, current, total, source } progress frames. Adapter wraps as { ready: Promise, close }. |
EventSource caveat: native EventSource cannot set an Authorization header. The sidecar gates these
streams by same-origin (the SPA is same-origin in production). For a cross-origin Lovable dev build you may
need to either (a) run behind a same-origin dev proxy to the sidecar, or (b) use a fetch-stream polyfill that
injects the Bearer header. The streams set Access-Control-Allow-Origin only for allowlisted origins.
2.3 WebSocket (optional) — GET /ws?token=<authToken>
Local sidecar /ws is an event-bus relay. Server→client frames { event, data } for
approval_required | step | tool | done | error | presence_update | notification. Client→server
{ type: 'approve'|'deny', requestId }. The SSE streams already cover the UI's needs, so WS is optional for
the rebuild. (The cloud-server /ws is a separate Clerk+Redis team-chat gateway — not used by the local UI.)
2.4 Hook pattern for streams
Mirror the existing domain hooks: each owns local state + opens its stream on mount, returns close on unmount.
// useEvents → subscribeEvents (/api/events/stream)
// useNotifications → /api/notifications/stream (unread count, markRead, markAllRead)
// useRoomState → subscribeSubagentStatus (named 'subagent_status' on /api/notifications/stream)
// useWaggleDance → subscribeWaggleDance (/api/waggle/stream) + publish/ack
// useChat → POST /api/chat fetch-stream → parses into ContentBlock[]
3. Global state & providers the rebuild needs
There is no Redux / Zustand / React Query. State = React Context (one provider) + custom hooks + the
singleton adapter + a window CustomEvent bus. Reproduce these four layers.
3.1 The one provider
| Provider | Provides | Behavior |
|---|---|---|
ServiceProvider / useService() |
{ adapter, connected, connecting, error, reconnect } |
calls adapter.connect() once on mount; wrap the whole app. (Plus shadcn TooltipProvider + a toast Toaster.) |
3.2 Domain hooks (the de-facto store)
Recreate these as the state layer — each wraps adapter calls + useState. The Desktop shell composes them.
| Hook | Owns |
|---|---|
useWorkspaces |
workspaces, active workspace, select/create/patch/delete/refresh — the workspace context every other call needs |
useChat |
messages (as ContentBlock[]), isLoading, sendMessage, clearHistory, pendingApproval, approveAction; parses the chat SSE stream |
useSessions |
per-workspace session CRUD (list/create/rename/delete/search/export) |
useMemory |
frames, stats, search, add/update/delete, incrementFrameAccess |
useKnowledgeGraph |
{ nodes, edges } |
useEvents |
audit steps + live SSE |
useNotifications |
notifications, unreadCount, markRead/markAllRead, live SSE |
useRoomState |
live sub-agent map (named subagent_status SSE) |
useWaggleDance |
signals, filter, ack, publish, live SSE |
useAgentStatus |
polls /api/agent/status |
useBilling |
tier, startCheckout, openPortal, syncAfterCheckout (auto-detects ?session_id= on load) |
useFeatureGate |
{ planTier, isEnabled, gate } — the per-feature gate front-end |
useOnboarding |
persisted OnboardingState in localStorage["waggle:onboarding"] (completed, step, tier, workspaceId, apiKeySet, templateId, personaId, tooltipsDismissed); honors ?skipOnboarding=true, ?forceWizard=true |
useOfflineStatus |
polls /health for the offline pill |
useProviders |
/api/providers |
useWindowManager |
window open/close/focus/minimize + per-window persona & autonomy |
useOverlayState |
boolean flags for every overlay |
useKeyboardShortcuts |
global hotkeys → open app/overlay (Cmd+K global search, etc.) |
3.3 The four pieces of "global context" the rebuild must thread
- Workspace context —
useWorkspaces.active. Nearly every call acceptsworkspace/workspaceId(alias-accepted; omit ⇒personalmind). The active workspace id flows into chat, memory, files, sessions, events. The window manager can override persona and autonomy per-window. - Session — the boot session token (
adapter.authToken) for transport auth, plus the per-workspace chat session id (useSessions, defaults to the workspace id then'default'). - Persona — workspace-default persona, overridable per-chat-window. Passed as
personain the chat body. - Tier —
useBilling.tier→getEffectiveTier→ capabilities, consumed byuseFeatureGate, the Dock, and the Upgrade/TrialExpired modals.
3.4 Cross-component window event bus (no library — wire these)
| Event | Dispatched by | Consumed by |
|---|---|---|
waggle:tier-insufficient { required, actual, message } |
adapter.fetch on 403 TIER_INSUFFICIENT |
UpgradeModal |
waggle:open-app { appId, tab? } |
apps (e.g. HarvestTab) |
Desktop → wm.openApp(appId) |
4. Screen-by-screen data contract
Each row = an OS app/overlay → the adapter methods/endpoints it calls → request/response summary. Paths are
verbatim and all /api/* are Bearer-gated. The AppId union (window content switch) is:
chat, dashboard, memory, events, capabilities, connectors, cockpit, mission-control, settings, vault, profile, terminal, calculator, notes, waggle-dance, files, agents, scheduled-jobs, marketplace, voice, room, approvals, timeline, backup, telemetry, governance, launcher.
(terminal/calculator/notes are declared but unimplemented; voice is a static "Coming Soon" placeholder.)
4.1 Dock apps
| Screen (appId) | Key endpoints | Request → Response summary |
|---|---|---|
Chat (chat) |
POST /api/chat (SSE); GET /api/history?workspace=&session=; DELETE /api/chat/history?session=; POST /api/approval/:id; POST /api/feedback; GET /api/memory/search?q=&scope=; pins GET/POST/DELETE /api/workspaces/:id/pins; POST /api/ingest; GET/PUT /api/agent/model; GET /api/settings; GET /api/team/members; PATCH /api/workspaces/:id |
Send {message,workspace,session?,persona?,autonomy?,shape} → SSE stream (§2.1). History → { sessionId, messages[{id,role,content,timestamp}], count }. Feedback {sessionId,messageIndex,rating,reason?,detail?} → fire-and-forget. Pin {messageContent,messageRole,label?}. Model PUT {model}. |
Dashboard / Home (dashboard) |
GET /api/memory/stats; GET /api/tasks (raw) |
stats → { personal, workspace, total } each {frames,entities,relations}. Tasks → task list. |
Memory (memory) — tabs: Frames, Knowledge Graph, Harvest, Wiki, Evolution |
frames GET/POST/PUT/DELETE /api/memory/frames, PATCH …/:id/access?workspace=; GET /api/memory/search?q=&scope=; GET /api/memory/graph?workspace= (or ?scope=all|personal); GET /api/memory/stats; Harvest: GET /api/harvest/sources, POST /api/harvest/scan-claude-code, POST /api/harvest/preview, POST /api/harvest/commit, GET /api/harvest/runs/latest-interrupted, POST /api/harvest/runs/:id/abandon, POST /api/harvest/extract-identity, GET /api/harvest/progress (SSE), PATCH/DELETE /api/harvest/sources/:source; Wiki: GET /api/wiki/pages, …/:slug, …/:slug/content, POST /api/wiki/compile, GET /api/wiki/health, POST /api/wiki/export/{obsidian,notion}; Evolution: GET/POST /api/evolution/{runs,runs/:id,run,targets,baseline,status} (raw); Weaver: GET /api/weaver/status, POST /api/weaver/trigger |
Frame (normalized) {id,content,source,frameType,importance,timestamp,score?,gop,accessCount,…}. Graph → { nodes:KGNode[], edges:KGEdge[] }. Harvest preview {data,source} → { knowledgeExtracted[] }; commit {data,source} / {resumeFromRun}. Wiki compile { mode:'incremental'|'full', concepts? }. |
Events & Logs (events) |
GET /api/events?workspaceId=; GET /api/events/stream (SSE) |
AgentStep[] + live audit events {id,timestamp,workspaceId,eventType,toolName?,input?,output?,model?,cost?,…}. |
Skills & Apps (capabilities) incl. Marketplace tab |
GET /api/skills; POST /api/skills/create; GET /api/skills/starter-pack/catalog; POST /api/skills/starter-pack/:skillId; GET /api/skills/capability-packs/catalog; GET /api/marketplace/packs; POST /api/marketplace/install; GET /api/audit/installs + GET /api/skills/test (raw) |
Skills SkillPack[] (forced installed:true). Install {packageId} → raw Response (403 → UpgradeModal). Create {name,description}. |
Connectors (connectors) |
GET /api/connectors; GET /api/connectors/:id/health; POST /api/connectors/:id/connect; POST /api/connectors/:id/disconnect; POST /api/vault |
ConnectorDefinition[] (card: {id,name,description,service,authType,status,capabilities[],substrate,tools[],logoUrl?,category?,setupGuide?}). Add secret {name,value,type?}. |
Cockpit / Command Center (cockpit) incl. Compliance |
GET /health; GET /api/agent/cost; GET /api/connectors; GET /api/cron; GET /api/vault; GET /api/capabilities/status; GET /api/audit/installs; GET /api/cost/summary; GET /api/weaver/status; GET /api/events/stats; Compliance: GET /api/compliance/status?workspaceId=, GET /api/compliance/templates, POST/PATCH/DELETE /api/compliance/templates*, POST /api/compliance/export, POST /api/compliance/export-pdf (Blob) |
health {status,uptime,services[]}; cost summary; event stats {totalEvents,period,byType,byDay,topTools}. |
Mission Control (mission-control) |
GET /api/fleet; GET /api/team/members; GET /api/team/activity; GET /api/tools/detect; POST /api/fleet/:workspaceId/(pause|resume|kill) |
Fleet (normalized) {workspaceId,workspaceName,personaId,model,status,lastActivity,duration,toolCount,tokenUsage,costEstimate} + {count,maxSessions}. |
Waggle Dance (waggle-dance) |
GET /api/waggle/signals; GET /api/waggle/stream (SSE); POST /api/waggle/signals; PATCH /api/waggle/signals/:id/ack |
WaggleSignal{id,type,workspaceId,content,metadata?,timestamp,acknowledged}. Publish {type,content,workspaceId?,metadata?}. |
Personas (Agents) (agents) |
GET /api/personas; POST /api/personas; PATCH/DELETE /api/personas/:id; POST /api/personas/generate; GET /api/capabilities/status; GET /api/agent-groups; POST /api/agent-groups; PATCH/DELETE /api/agent-groups/:id; POST /api/agent-groups/:id/run; GET /api/jobs/:jobId; POST /api/jobs/:jobId/cancel |
Persona create {name,description,icon?,systemPrompt,tools?}. Generate {prompt} → {name,description,systemPrompt,tools[]}. Group create {name,description?,strategy:'parallel'|'sequential'|'coordinator',members[{agentId,roleInGroup:'lead'|'worker',executionOrder}]}. Group run {task,teamId?} → queued stub. |
Files (files) |
GET /api/workspaces/:id/files/list?path=; POST …/files/upload (FormData, 30s); GET …/files/download?path= (Blob); POST …/files/{mkdir,delete,move,copy}; GET …/documents; GET …/documents/:name/versions |
FileEntry[]. Move/copy {from,to}; delete/mkdir {path}. Upload FormData(file,path). |
Scheduled Jobs (scheduled-jobs) |
GET /api/cron; POST /api/cron; PUT /api/cron/:id; DELETE /api/cron/:id; POST /api/cron/:id/trigger |
CronJob (normalized) {id,name,schedule,jobType,jobConfig,workspaceId,enabled,lastRun,nextRun,createdAt}. Create {name,cronExpr,jobType,jobConfig?,workspaceId?,enabled?}. Trigger → {triggered,autoEnabled?,schedule?}. |
Marketplace (marketplace) |
GET /api/marketplace/search?query=&limit=; GET /api/marketplace/installed; POST /api/marketplace/install; POST /api/marketplace/uninstall |
search/install/uninstall return raw Response for 403-aware handling. |
AI Tools / Launcher (launcher) |
GET /api/tools/detect; POST /api/tools/launch; GET /api/tools/processes; POST /api/tools/kill; POST /api/tools/hooks |
detect → {platform,detectedAt,tools[{id,displayName,installed,installedPath,version,hooksInstalled,…}]}. Launch {id,installedPath,workspaceId?,args?,cwd?} → {ok,pid,error?}. Hooks {id,action:'install'|'verify'|'uninstall',cliPath?}. |
Room (room) |
GET /api/notifications/stream named subagent_status (SSE) |
live sub-agent canvas {agents[{id,name,role,status:'pending'|'running'|'done'|'failed',task,toolsUsed,…}]}. |
Approvals (approvals, TEAMS+) |
GET /api/approval/pending; GET /api/approval/grants; POST /api/approval/:id; DELETE /api/approval/grants/:id; POST /api/approval/grants/clear |
pending {pending[{requestId,toolName,input,timestamp}],count}. |
Timeline (timeline) |
GET /api/events?workspaceId=&limit=&from= |
TimelineEvent[]. |
Backup & Restore (backup) |
GET /api/backup/metadata; POST /api/backup; POST /api/restore (all raw) |
backup streams application/octet-stream (.waggle-backup, ≤500MB). Restore { backup:<base64>, preview? }. |
Usage & Telemetry (telemetry) |
GET /api/cost/by-workspace; GET /api/events/stats (raw) |
cost-by-workspace + stats aggregates. |
Team Governance (governance, TEAMS+) |
none direct (props-driven sub-components) | renders governance UI. |
Settings (settings) |
GET/PUT /api/settings; GET/PUT /api/settings/permissions; POST /api/settings/test-key; GET /api/providers; GET /api/telemetry/status; POST /api/telemetry/toggle; DELETE /api/telemetry/events; GET /api/team/status; POST /api/team/{connect,disconnect}; GET /api/export, /api/debug/logs (raw) |
Settings object; permissions {defaultAutonomy,externalGates[],workspaceOverrides}. Test key {provider,apiKey} → {valid}. |
Vault (vault) |
GET /api/vault; POST /api/vault; DELETE /api/vault/:id; GET /api/connectors; POST /api/connectors/:id/(connect|disconnect) |
secret {name,value,type?}. Vault reveal is rate-limited to 5/min. |
My Profile (profile) |
GET/PUT /api/profile; POST /api/profile/analyze-style {text}; POST /api/profile/analyze-brand {description}; POST /api/profile/research {} |
profile object + analysis results. |
4.2 Overlays (rendered by Desktop, not windowed)
| Overlay | Key endpoints |
|---|---|
| Onboarding wizard (8 steps: Welcome, WhyWaggle, Tier, ModelTier, Import, Template, Persona, ApiKey, Ready) | connect, GET /api/vault, /health, GET /api/providers, /api/v1/models, harvest preview/commit, POST /api/harvest/scan-claude-code, import preview/commit, POST /api/personas, POST /api/vault, POST /api/workspaces, PUT /api/settings |
| Login briefing (session-start digest) | GET /api/identity, GET /api/workspaces, GET /api/memory/search, GET /api/memory/stats, GET /api/workspaces/:id/context |
| Global search (Cmd+K) | GET /api/workspaces, sessions, GET /api/skills, GET /api/memory/search |
| Create workspace dialog | GET /api/browse/local?path=, POST /api/browse/local/mkdir, GET/POST/PUT/DELETE /api/workspace-templates*, POST /api/workspace-templates/generate, GET /api/connectors, GET /api/agent-groups |
| Persona switcher | GET /api/personas, GET /api/agent-groups |
| Spawn agent dialog | GET /api/litellm/models, /api/litellm/pricing, GET /api/providers, GET /api/agent/model, POST /api/workspaces, POST /api/fleet/spawn ({task,persona?,model?,parentWorkspaceId?}) |
| Erase data dialog (GDPR) | POST /api/data/erase — header X-Confirm-Erase: yes + body { confirmation:'I UNDERSTAND THIS IS PERMANENT' } |
| Upgrade modal | triggered by waggle:tier-insufficient; actions → POST /api/tier/start-trial, POST /api/stripe/create-checkout-session {tier:'PRO'|'TEAMS', billingPeriod?} → {url} |
| Trial expired modal | POST /api/stripe/create-checkout-session |
| Workspace switcher / Notification inbox / Context rail / Keyboard help / Tooltips | props-driven or lib/context-rail-fetch.ts; no/minor direct calls |
4.3 Billing / tier endpoints (used across overlays + useBilling)
| Method | Path | Body → Response |
|---|---|---|
| GET | /api/tier |
→ { tier, trialDaysRemaining?, trialExpired?, capabilities, usage } |
| POST | /api/tier/start-trial |
→ { tier, rawTier, trialStartedAt, trialDaysRemaining, trialExpired, capabilities } (409 if already started) |
| POST | /api/stripe/create-checkout-session |
{ tier:'PRO'|'TEAMS', billingPeriod?:'monthly'|'annual' } → { url } (503 STRIPE_NOT_CONFIGURED if unset) |
| POST | /api/stripe/sync |
{ sessionId } → { tier, customerId } — call this after the checkout redirect (webhooks unreliable behind NAT) |
| POST | /api/stripe/create-portal-session |
— → { url } (requires PRO+) |
Stripe flow for the rebuild: open {url} from create-checkout-session → user pays → Stripe redirects to
/payment-success?session_id=... → useBilling detects ?session_id= on load → POST /api/stripe/sync →
refresh tier. Render upgrade UI defensively when 503 STRIPE_NOT_CONFIGURED.
5. Design system — Hive DS (dark default, desktop-OS metaphor)
The visual contract lives in apps/web/src/index.css (@layer base source of truth) + waggle-theme.css
(component aliases). It's a shadcn-style HSL-variable system + a literal Hive palette, with a [data-theme="light"]
override. Default theme is dark. Tailwind 4 + shadcn primitives + lucide icons.
5.1 Core brand tokens (the three the brief names)
| Token | Dark value | Role |
|---|---|---|
Honey --honey-500 |
#e5a000 |
Primary brand / accent / focus ring / --primary (40 100% 45%) |
Hive-950 --hive-950 |
#08090c |
Deepest background / status bar; --background ≈ 222 20% 4% |
Accent (AI) --status-ai / --accent |
#a78bfa / 270 60% 68% |
Secondary accent (AI, knowledge-concept highlights) |
5.2 Full palette (use the CSS variables, never hard-code hex)
- Hive grays (cold undertone):
--hive-950 #08090c → --hive-50 #f0f2f7(12 steps). Surfaces:--surface-card: var(--hive-850),--surface-panel: var(--hive-800),--surface-overlay: rgba(8,9,12,0.88). - Honey scale:
--honey-600 #b87a00 … --honey-50 #fffbeb, plus--honey-glow rgba(229,160,0,0.12),--honey-pulse rgba(229,160,0,0.06). - Status:
--status-healthy #34d399,--status-warning #fbbf24,--status-error #f87171,--status-info #60a5fa,--status-ai #a78bfa. - Knowledge-graph nodes:
--kg-person #4A90D9,--kg-project #50C878,--kg-concept #9B59B6,--kg-org #E67E22,--kg-default #95A5A6. - shadcn semantic vars (HSL triplets, consumed via
hsl(var(--x))):--background, --foreground, --card, --popover, --primary (40 100% 45%), --secondary, --muted, --accent (270 60% 68%), --destructive, --border, --input, --ring (40 100% 45%), --radius 0.75rem. Sidebar + chart vars mirror these. - Step/event colors (event stream):
--step-running/-success/-pending/-error/-thinking/-search/-web/-tool/-writing.
5.3 Typography, radius, shadows, motion
- Fonts: headings
Space Grotesk; bodyDM Sans(fallback Inter/system); monoJetBrains Mono. Imported from Google Fonts inindex.css.--font-sans,--font-monoaliases exist. - Type scale:
--text-micro 11 → --text-display 24(micro 11, caption 12, body-sm 13, body 14, title 16, heading 20, display 24). - Radius:
--radius: 0.75rem. - Shadows:
--shadow-card,--shadow-elevated,--shadow-overlay,--shadow-honey(honey glow),--shadow-focus(2px honey ring). - Signature motifs: glassmorphism (
.glass/.glass-strong— backdrop blur 20–30px), honeycomb hex background (.honeycomb-bg, SVG data-URI at 3% honey opacity), hex avatar clip-path (.hex-avatar), hex streaming cursor (.hex-cursor), honey-pulse on memory-save, heartbeat on health dot, float on the bee mascot, token-fade on streamed text. Selection + thin 5px scrollbars are honey/hive themed. - Interaction utilities:
.waggle-interactive,.waggle-card-lift,.waggle-nav-hover,.waggle-press,.direction-d-card(the canonical card: hive-700 border → honey-500 + honey-shadow on hover).
5.4 Light mode
:root[data-theme="light"] flips the hive scale (cream #fdfcf9 bg, dark text), darkens honey + status +
KG colors for WCAG AA on cream, and softens shadows. Toggle by setting data-theme="light" on :root.
Ship dark first; light is a polish pass.
5.5 Layout — the desktop OS metaphor
- Desktop: full-viewport,
overflow:hiddenbody, honeycomb wallpaper +.desktop-overlaywash. - Dock: launcher rail (bottom/side) built from
lib/dock-tiers.ts. Entries areapp | zone-parent (collapsible group) | separator, each withicon(lucide),label,color.getDockForTier(userTier, billingTier)filters byminBillingTier(e.g.governance&approvalsare TEAMS+; empty zone-parents are dropped).simpletier shows ~6 apps;power/adminshow the full set incl. Ops + Extend zone groups. - Windows: each open app is a draggable/resizable
AppWindowkeyed byappId; content via arenderAppContent(appId)switch. Windows carry per-window persona + autonomy. - Overlays: modals/rails/wizards rendered directly by Desktop, gated by
useOverlayStateflags. - BootScreen: shown until
localStorage["waggle-booted"], then Desktop mounts.
6. Prioritized rebuild order
Build in dependency order. Each phase is independently demoable.
Phase 0 — Foundation (nothing renders without this).
adaptersingleton: base URL +localStorage["waggle:server-url"],connect()(health probe + session-token bootstrap), thefetchwrapper (conditional content-type, Bearer, 403→waggle:tier-insufficient, 10s/30s timeouts, 429 handling), and the §1.4 read normalizers.ServiceProvider/useService()callingadapter.connect()once.- Hive DS tokens (
index.css+waggle-theme.css), dark default, fonts, shadcn vars. - The
windowevent bus (§3.4).
Phase 1 — Shell.
5. Desktop shell: useWindowManager (open by appId), Dock from dock-tiers.ts, AppWindow + renderAppContent
switch, BootScreen, useOverlayState, useKeyboardShortcuts (Cmd+K).
6. useWorkspaces (active workspace context) + Workspace switcher + Create-workspace dialog.
7. useBilling + tier gating (getEffectiveTier → capabilities → useFeatureGate) + Upgrade/TrialExpired modals.
Phase 2 — The product's core loop (Chat).
8. Chat (chat) end-to-end: POST /api/chat fetch-stream parser → ContentBlock[], all SSE events,
inline approvals (POST /api/approval/:id), history, model switch, pins, feedback. This is the single
highest-value screen — do it first and well.
9. Dashboard / Home (dashboard) — cheap, gives a landing surface (/api/memory/stats, /api/tasks).
Phase 3 — Memory moat (the strategic lock-in).
10. Memory (memory): Frames + Knowledge Graph tabs first, then Harvest (import is the moat: scan +
preview/commit + progress SSE), then Wiki, then Evolution.
11. Files (files) — workspace file CRUD + upload/download.
Phase 4 — Real-time ops + agents.
12. Events (events, SSE), Room (room, subagent_status SSE), Waggle Dance (waggle-dance, SSE).
13. Personas/Agents (agents) + Spawn agent dialog + Mission Control (mission-control, fleet).
14. Scheduled Jobs (scheduled-jobs), Notifications inbox.
Phase 5 — Extensibility + monetization surfaces.
15. Skills & Apps (capabilities) + Marketplace (marketplace) + Connectors (connectors) +
Vault (vault) — these are the upgrade triggers; wire 403→Upgrade carefully.
16. Settings (settings), My Profile (profile), Cockpit (cockpit) + Compliance.
Phase 6 — Governance / admin / polish.
17. Approvals (approvals, TEAMS+), Team Governance (governance, TEAMS+), Timeline (timeline),
Telemetry (telemetry), Backup (backup), Erase data (GDPR).
18. Onboarding wizard (8 steps) + Login briefing + Global search polish.
19. Light mode pass.
Rationale: Phases 0–1 are non-negotiable scaffolding. Chat (Phase 2) is the product. Memory/Harvest (Phase 3) is the strategic moat ("free forever" lock-in). Real-time + agents (Phase 4) prove the "OS" thesis. Monetization surfaces (Phase 5) are where tier gating earns money. Governance + polish (Phase 6) come last.
7. Gotchas checklist (the things that bite a rebuild)
- Chat is POST-SSE, not
EventSource— parse the body stream manually. - Body-less POSTs must not send
Content-Type: application/json. EventSourcecan't send Bearer headers — the SSE streams are same-origin gated; proxy or polyfill in dev.- Run tier through
getEffectiveTierbefore gating (expired TRIAL → FREE). -1in any capability/limit means unlimited, not "zero/disabled".- Reproduce the §1.4 read normalizers or the UI breaks on renamed fields.
- Echo back
sourceWorkspaceIdverbatim when resolving approvals. - After Stripe redirect, call
POST /api/stripe/sync { sessionId }— do not trust the webhook for desktop. - Handle two
doneusage shapes (agent-loop vs echo/command path). /api/notifications/stream:notificationis an unnamed frame (onmessage); the others are named events.- All
/api/*paths are flat — no plugin prefix; the literal path in the table IS the path. - Respect rate limits (chat 120/min, vault reveal 5/min, backup/restore 2/min) — handle
429+Retry-After.