191
docs/redesign-warm-hive/BUILD-PLAN.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Warm-Hive Redesign — Build Plan (DRAFT, pending founder confirmation)
|
||||
|
||||
> Source design package: `docs/design_handoff_waggle_app/` (README.md, SCREENS.md,
|
||||
> DESIGN_POV.md, `design-files/styles/waggle.css`, 19 HTML refs + screenshots).
|
||||
> This plan recreates that concept **inside the existing `apps/web` stack** (React 19 +
|
||||
> TS + Vite + Tailwind 3 + shadcn/ui + React Router 6) — not by pasting the HTML.
|
||||
>
|
||||
> **Status:** proposal. No feature code until the open decisions in §7 are confirmed.
|
||||
|
||||
---
|
||||
|
||||
## 1. The concept in one paragraph
|
||||
|
||||
A warmer **"Hive"** identity that fixes five live-product problems: overloaded nav
|
||||
(~18 destinations), engineer-first language, flat 10–13px density, five semantic colors
|
||||
firing at once, and buried differentiators. The fixes: a **5-item calm spine + ⌘K
|
||||
command bar** (progressive disclosure), **plain labels with the technical term as a mono
|
||||
subtitle**, a **single honey accent** with desaturated status-only semantics, **warm
|
||||
graphite ↔ warm paper** themes, and **"hero moments"** for memory / overnight work /
|
||||
coordination. Memory is sold as a trustworthy, *editable, accountable* asset — the
|
||||
**Memory-Trust layer** (DESIGN_POV #1, already designed) is the keystone.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current state → target gap (grounded in codebase recon)
|
||||
|
||||
| Area | Current reality (verified) | Target | Gap size |
|
||||
|---|---|---|---|
|
||||
| **Tailwind** | **3.4.17**, JS config `apps/web/tailwind.config.ts` (CLAUDE.md's "Tailwind 4" is stale; root has the `@tailwindcss/vite` plugin but web uses TW3) | n/a — keep TW3, remap token *values* | none (no migration) |
|
||||
| **Theme infra** | `data-theme="light"` attribute + `localStorage['waggle-theme']`; boot in `App.tsx`, toggle in `SettingsApp.tsx`, reactivity via MutationObserver in `AppShell.tsx` + `useIsLightTheme.ts`. `next-themes` installed but **unused**. | A real **ThemeProvider** owning `data-theme` + persistence + system pref | medium (consolidate scattered logic; infra already matches design's `[data-theme]` selector) |
|
||||
| **Tokens** | hive-grays (12) + honey (7) + status (5) + KG colors; cooler palette (`--hive-950:#08090c`, `--honey-500:#e5a000`). shadcn core in HSL. | warm Hive set from `waggle.css` (`--bg:#14110b`, `--honey:#e9a52c`, desaturated semantics, +wash/line/glow, r-sm..r-xl, 4 shadows) | medium (values swap + add missing tokens) |
|
||||
| **Fonts** | Space Grotesk (display) + DM Sans (body) + JetBrains Mono | **Hanken Grotesk** (display+body) + JetBrains Mono | small |
|
||||
| **UI lib** | **shadcn/ui fully installed** — `components.json`, 57 `ui/` components, `cn()`, CVA, Radix, lucide, framer-motion, sonner, cmdk. (`base-ui` at root but unused.) | same — style shadcn primitives to the tokens | none (design's shadcn assumption holds) |
|
||||
| **Shell** | `AppShell.tsx` w-52 left nav rendering the **full 5-zone tree** (Work/Intelligence/Extend/Team/System, ~18 destinations) via `dock-tiers.ts` | **5-item spine** (Home, Chat, Memory, Agents & tasks, Library) + workspace switcher + user row; everything else → ⌘K | large (IA collapse) |
|
||||
| **⌘K** | `CommandCenter.tsx` exists — cmdk, Ctrl/Cmd-K, 6 verb groups, backend search | regroup to **Jump to / Do / Power tools**, plain-name + mono subtitle, first-result auto-select, **Pro "★ Pinned"** group | medium (rework existing) |
|
||||
| **Routing** | React Router 6.30, ~28 routes under `AppShell` layout, `routeFor(appId, ctx)` | unchanged; spine items map onto existing routes | none |
|
||||
|
||||
**Net:** the *plumbing* is in great shape (data-theme strategy, shadcn, cmdk, routing
|
||||
all align with the design). The real work is (a) a faithful **token + font swap**, (b) a
|
||||
**ThemeProvider** to replace ad-hoc DOM code, (c) **collapsing the visible nav to 5 + ⌘K**,
|
||||
then (d) rebuilding screens to the specs.
|
||||
|
||||
---
|
||||
|
||||
## 3. Token mapping (PR1) — `waggle.css` → `apps/web`
|
||||
|
||||
**Strategy:** make the design's **named tokens the source of truth** in `index.css`
|
||||
(`:root` dark + `:root[data-theme="light"]` light), set to the exact hex from
|
||||
`waggle.css`, then point the shadcn HSL core tokens and the existing hive/honey scales at
|
||||
those warm values so the 57 themed components restyle automatically.
|
||||
|
||||
### 3.1 Named tokens (verbatim from `waggle.css` §7) — add to `index.css`
|
||||
- Surfaces: `--bg --bg-2 --surface --surface-2 --surface-3`
|
||||
- Lines: `--line --line-soft --line-strong`
|
||||
- Text: `--text --text-2 --text-muted --text-dim`
|
||||
- Honey: `--honey --honey-bright --honey-deep --honey-wash --honey-line --honey-glow`
|
||||
- Semantics: `--work --intel --healthy --attention --risk` + each `*-wash`
|
||||
- Shadows: `--shadow-sm --shadow --shadow-lg --shadow-pop` (warm light variants)
|
||||
- Radii: `--r-sm:8 --r:12 --r-lg:18 --r-xl:26`; pills/toggles `999px`
|
||||
- Type: `--sans` (Hanken Grotesk) · `--mono` (JetBrains Mono) · `--serif: var(--sans)`
|
||||
|
||||
### 3.2 shadcn HSL core → derive from warm palette (recolor)
|
||||
Convert warm hex → HSL channels (e.g. `--bg #14110b → --background: 40 29% 6%`):
|
||||
`--background←--bg` · `--foreground←--text` · `--card/--popover←--surface` ·
|
||||
`--secondary/--accent/--muted (surface)←--surface-2` · `--muted-foreground←--text-muted` ·
|
||||
`--border/--input←--line` · `--ring←--honey` · `--primary←--honey` with
|
||||
`--primary-foreground:#1a1407` · `--destructive←--risk`. Keep the existing
|
||||
`hive-*`/`honey-*` Tailwind scales but recolor their CSS vars to the warm steps.
|
||||
|
||||
### 3.3 Fonts
|
||||
Swap the Google Fonts `@import` to **Hanken Grotesk (300–800) + JetBrains Mono (400–600)**;
|
||||
set `body`/`--font-sans` to Hanken, `h1–h6` display to Hanken 600 (`-0.02..-0.03em`),
|
||||
keep `--font-mono`. (Self-host in the Tauri/Platform pass for offline — follow-up.)
|
||||
|
||||
### 3.4 Utilities (port into `waggle-theme.css`, reconcile with existing)
|
||||
`.hex` clip-path (reconcile with existing `.hex-avatar`) · `.comb` honeycomb data-URI ·
|
||||
`.dot-live` + `@keyframes breathe` (reconcile with existing `honey-pulse`) ·
|
||||
`:focus-visible` honey outline · `::selection` honey · warm scrollbar · `.pill` `.kbd`.
|
||||
|
||||
### 3.5 PR1 verification
|
||||
`tsc -p apps/web/tsconfig.app.json` 0 errors · `npm run test` (FE) green · `npm run lint`
|
||||
clean · visual smoke: dark default + light toggle on Home/Chat/Settings, no contrast
|
||||
regressions (re-run `light-mode-tokens.test.ts`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Theme provider (PR1)
|
||||
|
||||
New `apps/web/src/providers/ThemeProvider.tsx`: context owning `'dark' | 'light' | 'system'`,
|
||||
writes `data-theme` + `localStorage['waggle-theme']`, subscribes to
|
||||
`matchMedia('(prefers-color-scheme)')` when `system`, honors `prefers-reduced-motion` for
|
||||
entrance gating. Exposes `useTheme()`. **Refactor:** remove the boot snippet in `App.tsx`,
|
||||
the toggle logic in `SettingsApp.tsx`, and re-back `useIsLightTheme()` with the context
|
||||
(keep its signature). Recommend a **small custom context** over `next-themes` (Vite SPA,
|
||||
not Next; keeps the exact `data-theme` contract the design's CSS already targets).
|
||||
|
||||
---
|
||||
|
||||
## 5. App shell — 5-item spine + ⌘K (PR2)
|
||||
|
||||
### 5.1 Sidebar (`ia.html`)
|
||||
Replace the zone-tree render in `AppShell.tsx:272–303` with:
|
||||
**workspace switcher pill** (hex + name + chevron) → **5 nav items** with honey active
|
||||
state (left honey bar + `--honey-wash`) → "Everything else" **⌘K tile** → spacer →
|
||||
**user row** (avatar + name → Settings). Spine → routes:
|
||||
|
||||
| Spine item | Route (initial) | Later combined surface |
|
||||
|---|---|---|
|
||||
| Home | `/home` | — |
|
||||
| Chat | active workspace chat `routeFor('chat', ctx)` → `/workspaces/:id/chat` (fallback `/home`) | — |
|
||||
| Memory | `/memory` | + per-workspace Memory tab |
|
||||
| Agents & tasks `[badge]` | `/agents` (badge = pending approvals) | tabs: Agents · Automations · Approvals |
|
||||
| Library | `/artifacts` | tabs: Artifacts · Files · Skills |
|
||||
|
||||
Everything else (waggle-dance, connectors, MCP, marketplace, launcher, room, vault,
|
||||
mission-control, timeline, usage, team, evolution, benchmark, platform) → **⌘K only**.
|
||||
**Pro mode** (tier-aware toggle) inserts a "Pinned · power tools" group (Agent swarm,
|
||||
Connectors, Approvals). Pro-pinned may be deferred to a PR2 follow-up.
|
||||
|
||||
### 5.2 ⌘K (`CommandCenter.tsx` rework)
|
||||
Regroup to **Jump to / Do / Power tools**; each result = icon + **plain name** + **mono
|
||||
subtitle** (technical term) + optional shortcut; first result auto-selected; **Pro "★
|
||||
Pinned · Pro"** group prepended in Pro mode. Reuse cmdk + the existing adapter search.
|
||||
Copy patterns from SCREENS §05 ("Run a team of agents · waggle-dance · swarm", "Connect a
|
||||
tool · MCP servers · 21 tools", "Launch a coding agent · Claude Code · Cursor · Codex").
|
||||
|
||||
### 5.3 PR2 verification
|
||||
tsc/test/lint green · **live smoke**: every spine item routes; ⌘K opens (Ctrl/Cmd-K),
|
||||
fuzzy filters, arrow/Enter/Esc nav, routes + closes; all hidden destinations reachable
|
||||
from ⌘K; active-state highlight via `matchNavRoute`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phased roadmap (follows README §12; ship dark first)
|
||||
|
||||
| PR | Scope | Key files | Screens |
|
||||
|---|---|---|---|
|
||||
| **PR1** | Tokens → TW theme + ThemeProvider + fonts | `index.css`, `tailwind.config.ts`, `waggle-theme.css`, `providers/ThemeProvider.tsx`, `App.tsx`, `SettingsApp.tsx`, `useIsLightTheme.ts` | (foundation) |
|
||||
| **PR2** | App shell: 5-item sidebar + ⌘K rework | `AppShell.tsx`, `dock-tiers.ts`, `CommandCenter.tsx` | 05 |
|
||||
| **PR3** | Home (A Editorial) · Chat (B split-canvas) · Workspace (A overview+tabs) | `HomeCockpit`, `ChatWindowInstance`, `WorkspaceChatApp` | 01·02·03 |
|
||||
| **PR3.5** | **Memory-Trust layer** (DESIGN_POV #1) — confidence/freshness + forget/correct + stale-review + "why did you do that?" trace | `MemoryCenterApp`, memory adapter, `confidence-badge`/`evidence-*` (exist) | 19 |
|
||||
| **PR4** | Marketplace + **shared install store ("sync")** (grid + agent-pick + inline-in-chat) | `MarketplaceApp`, new install store | 09 |
|
||||
| **PR5** | Settings (models-first + failover pilot) · Onboarding (6-step + **model gate**) | `SettingsApp`, onboarding wizard | 11·10 |
|
||||
| **PR6** | Remaining surfaces: Launcher · Storage/Files · Power surfaces · App-surfaces sextet · Evolution · Benchmark · Platform · Habit-on-Home | many | 06·07·08·16·12·17·18·15·04 |
|
||||
| **PR7** | Auth (Clerk themed) · Billing (Stripe themed) | `auth`, `billing` | 13·14 |
|
||||
| **PR8** | `apps/www` landing (Next.js) — full content + identity | `apps/www` | Landing |
|
||||
|
||||
> **Memory-Trust early (PR3.5):** DESIGN_POV says its primitives "should land early because
|
||||
> everything else trades on them." Memory is a spine item, so it surfaces in PR2/PR3 anyway —
|
||||
> wiring trust right after gives the differentiator a real home before the long tail.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open decisions (need founder confirmation before implementing)
|
||||
|
||||
1. **First-PR scope** — split **PR1 (tokens + ThemeProvider + fonts)** then **PR2 (shell +
|
||||
⌘K)** *(recommended — tokens land first, lower risk per PR)*, or one combined PR?
|
||||
2. **IA collapse** — confirm reducing the always-visible nav from ~18 → **5-item spine +
|
||||
⌘K**, Settings via user row, power features ⌘K-only (+ optional Pro-pinned). Anything
|
||||
that **must** stay always-visible beyond the 5?
|
||||
3. **Memory-Trust placement** — land **early (PR3.5, right after shell)** *(recommended)*
|
||||
or after the core screens?
|
||||
4. **Recommend-and-proceed unless you object:** ThemeProvider = small custom context (not
|
||||
`next-themes`); fonts via Google Fonts now / self-host in Platform pass; keep Tailwind 3.
|
||||
5. **Flag for later (blocks Billing/PR7, not now):** DESIGN_POV #4 — **who pays for
|
||||
inference** (BYO-key vs Waggle-metered). Decide before PR7.
|
||||
|
||||
---
|
||||
|
||||
## 8. Notes / discrepancies surfaced (honesty log)
|
||||
- CLAUDE.md §1 lists **Tailwind 4** and **base-ui/react**; `apps/web` actually runs
|
||||
**Tailwind 3.4.17** and **shadcn/ui + Radix** (base-ui unused in web). No action — just
|
||||
don't trust those two CLAUDE.md lines for this work.
|
||||
- `next-themes` is a dependency but unused; PR1 either adopts or removes it.
|
||||
- All 19 screen HTMLs + per-screen "ship this variation" notes are in
|
||||
`docs/design_handoff_waggle_app/SCREENS.md` — consult per PR.
|
||||
|
||||
## 9. PR1 adversarial-review follow-ups (deferred LOW, tracked)
|
||||
A 17-agent adversarial review of PR1 confirmed 10 findings; 8 were fixed in-PR
|
||||
(1 HIGH light-mode honey-button contrast + WCAG ratchet extension, 1 MEDIUM
|
||||
spine/pinned `/approvals` double-active collision, duplicate Agents/swarm badge,
|
||||
unbacked ⌘K shortcut hints, platform-aware ⌘K glyph). Two LOW items are
|
||||
deferred to PR3 with rationale:
|
||||
- **Chat spine item, no-workspace state** — `routeFor('chat')` falls back to
|
||||
`/home` when there's no real workspace, so the clicked Chat item isn't
|
||||
highlighted (Home wins). Semi-intentional: Home **is** the workspace selector
|
||||
(`routes.ts` §9.7). PR3 can dim Chat or route it to the workspace switcher
|
||||
when `!hasRealActiveWorkspace`.
|
||||
- **Sidebar user row `userName={null}`** — renders "Account" + "W" avatar. A real
|
||||
display name exists in the `/api/home/briefing` contract (`HomeBriefing.userName`);
|
||||
thread it through when the user-identity surface lands (PR3).
|
||||
407
docs/redesign-warm-hive/PR3-BUILD-PLAN.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# PR3 — Build Plan · Warm-Hive Redesign · Screens 01 / 02 / 03
|
||||
|
||||
> Rebuilds three screens to the warm-Hive design **inside** the existing `apps/web`
|
||||
> stack (React 19 + TS + Vite + Tailwind 3 + shadcn/ui + React Router 6). Recreate the
|
||||
> concept — do **not** paste the HTML. One branch, one PR, commit-per-screen.
|
||||
>
|
||||
> Synthesised from the four recon files
|
||||
> (`docs/redesign-warm-hive/pr3-recon/{home,chat,workspace,primitives}.md`),
|
||||
> `BUILD-PLAN.md` §6, and `docs/design_handoff_waggle_app/{SCREENS.md §01/02/03, DESIGN_POV.md}`.
|
||||
> Citations are `file:line` against the branch `feature/warm-hive-redesign`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Scope + ground rules
|
||||
|
||||
### In scope (3 screens, ship-variation locked)
|
||||
- **Screen 01 — Home / Cockpit → Variation A "Editorial"** (`HomeCockpit.tsx`).
|
||||
- **Screen 02 — Chat / agent runtime → Variation B "Split work canvas"** (`ChatApp.tsx`
|
||||
inside the kept-alive `ChatHost` portal subtree).
|
||||
- **Screen 03 — Workspace → Variation A "Overview + tabs", Memory stays a tab**
|
||||
(`WorkspaceDesktopApp.tsx` + `WorkspaceRoute.tsx`).
|
||||
|
||||
### Out of scope (do NOT build here)
|
||||
- **PR3.5 — Memory-Trust layer (screen 19, DESIGN_POV #1).** Confidence/freshness on
|
||||
every memory, Forget/Correct, stale-review, "why did you do that?" trace. Separate
|
||||
later PR. **PR3 hooks to leave** (cheap, non-fabricating):
|
||||
- Use the existing `ConfidenceBadge` + `EvidenceChip`/`EvidencePanel`/`DetailDrawer`
|
||||
primitives (primitives.md §2) for fact rows and activity-stream provenance, so PR3.5
|
||||
can attach forget/correct affordances to the **same** components rather than re-laying
|
||||
them out.
|
||||
- Make every "What Waggle knows" fact row (Workspace) and every memory step (Chat)
|
||||
**clickable into a `DetailDrawer`** placeholder (or no-op with a `data-memory-id`
|
||||
attribute) so PR3.5 wires the trace/correct flow without a re-layout.
|
||||
- Keep the J08 review banner + `home-cockpit-review-cta` event payload intact (it is the
|
||||
Home entry-point into the unreviewed-memory queue PR3.5 expands).
|
||||
- **Do not** invent confidence/freshness numbers in PR3 — render provenance/confidence UI
|
||||
only where real data exists (see §5); leave the affordance, not fake data.
|
||||
- **PR4+ surfaces** (Marketplace, Settings, Onboarding, Launcher, Power surfaces, etc.) —
|
||||
untouched. PR3 only restyles the three named screens; the 5-item spine + ⌘K (PR2) and
|
||||
tokens/ThemeProvider/fonts (PR1) are **already shipped** — reuse, do not re-author.
|
||||
|
||||
### Two deferred PR1 LOW items to clear in PR3 (BUILD-PLAN §9)
|
||||
1. **Chat spine no-workspace dead-click.** `routeFor('chat')` falls back to `/home` when
|
||||
there is no real active workspace, so the Chat spine item isn't highlighted (Home
|
||||
wins) and the click feels dead. **Fix in PR3:** when `!hasRealActiveWorkspace`, route
|
||||
the Chat spine item to the workspace switcher / Home selector and/or dim it. Touch
|
||||
`Sidebar.tsx` consumer wiring (the spine-item builder), not the screen bodies.
|
||||
2. **Sidebar user-row `userName={null}`** renders "Account" + "W". A real name exists in
|
||||
`HomeBriefing.userName` (home.ts:261-270). **Fix in PR3:** thread `briefing.userName`
|
||||
through to the Sidebar user row once Home loads the briefing (or via the existing
|
||||
identity surface). This is the "user-identity surface lands (PR3)" note.
|
||||
|
||||
### Execution model + process
|
||||
- **In-place refactor of `apps/web`.** No Lovable, no new app. Restyle/replace the render
|
||||
bodies of the three existing components; keep route wrappers, data hooks, load() states,
|
||||
and all test-locked testids/behaviours.
|
||||
- **One branch (`feature/warm-hive-redesign`), one PR, commit-per-screen.** Suggested
|
||||
commits: `Phase 0 primitives`, `Phase A Home`, `Phase B Chat`, `Phase C Workspace`,
|
||||
`LOW fixes (chat no-ws + userName)`, then verification fixups.
|
||||
- **Dark-first** (BUILD-PLAN §6); light mode inherits via tokens — every new color must
|
||||
resolve from a `var(--token)` so the WCAG ratchet (`light-mode-tokens.test.ts`) stays
|
||||
green. Honor `prefers-reduced-motion` on every animated atom.
|
||||
- **Reuse > rebuild; degrade gracefully on missing data; surgical changes** (CLAUDE.md §3).
|
||||
|
||||
---
|
||||
|
||||
## 1. Phase 0 — Shared primitives first (build BEFORE the screens)
|
||||
|
||||
All warm tokens + utility classes the design references **already exist** (primitives.md
|
||||
§1: `--bg --surface* --line* --text* --honey* --work --intel --healthy --attention --risk`
|
||||
+ washes, `--r/-lg/-xl`, shadows, `.hex`, `.comb`, `.dot-live`, Hanken/JetBrains).
|
||||
**Do not re-add tokens.** Build only the **net-new composite atoms** (primitives.md §3)
|
||||
that no component provides today, in a **new folder `apps/web/src/components/os/warm/`**.
|
||||
Keep each < 80 LOC, token-driven, a11y-labeled, `prefers-reduced-motion`-safe.
|
||||
|
||||
**Build order** (shared atoms first, then screen-specific composites):
|
||||
|
||||
| # | Primitive | File | Minimal prop API | Consumed by |
|
||||
|---|---|---|---|---|
|
||||
| 0.1 | `HexAvatar` | `os/warm/HexAvatar.tsx` | `label: string; size?: number; gradient?: boolean; className?` — `.hex` clip + honey gradient + `#1a1407` initial; extract from `Sidebar.tsx:116` | **Home** (ws cards), **Chat** (context header + bot avatar), **Workspace** (46px header) |
|
||||
| 0.2 | `SectionLabel` | `os/warm/SectionLabel.tsx` | `children; className?` — 11px mono, uppercase, `.12–.14em` tracking, `--text-dim`, trailing hairline rule (ref `Sidebar.tsx:102`) | all 3 screens |
|
||||
| 0.3 | `DotLive` | `os/warm/DotLive.tsx` | `tone?: 'healthy'\|'attention'\|'risk'\|'work'\|'intel'\|'honey'; className?` — colored dot + `.dot-live` breathe; `prefers-reduced-motion` | **Home** greeting, **Chat** model pill, **Workspace** "agent live" |
|
||||
| 0.4 | `ProvenanceLine` | `os/warm/ProvenanceLine.tsx` | `source: string; when?: string; onClick?` — `⬡ source · when`, mono `--intel`; thin wrapper over `EvidenceChip` styled to `--intel` | **Workspace** fact + recent-work rows; **Chat** activity steps |
|
||||
| 0.5 | `RunChip` | `os/warm/RunChip.tsx` | `label: string; tone?: StatusTone` — status dot + label inline chip | **Home** overnight hero |
|
||||
| 0.6 | `IconTile` | `os/warm/IconTile.tsx` | `icon: ElementType; tone?: StatusTone; size?: number` — tinted (`*-wash`) rounded icon tile | **Home** "Waggle suggests"; **Workspace** recent-work ext tiles |
|
||||
| 0.7 | `HexCheckTile` | `os/warm/HexCheckTile.tsx` | `tone?; size?` — small `.hex` tile w/ check glyph | **Workspace** "What Waggle knows" fact rows |
|
||||
| 0.8 | `StreakChip` | `os/warm/StreakChip.tsx` | `days: number; weekDots?: boolean[]; className?` — 🔥 + "N-day streak", `--honey-wash`/`--honey-line` | **Home** greeting (mock data — §5) |
|
||||
| 0.9 | `ModelPill` | `os/warm/ModelPill.tsx` | `mode?: string; model: string; onClick?; title?` — "auto · Claude Sonnet" pill + healthy `DotLive` | **Chat** context header |
|
||||
| 0.10 | `OvernightHero` | `os/warm/OvernightHero.tsx` (Home composite) | `eyebrow; statement: ReactNode; runs: RunChipProps[]` — `--r-xl` gradient card + honey radial glow; renders gracefully with `runs=[]` | **Home** (composes `RunChip`) |
|
||||
| 0.11 | `AskBar` | `os/warm/AskBar.tsx` | `placeholder?; onSubmit: (text) => void; cmdkHint?: boolean; onPlus?` — full-width pill, honey `+`, ⌘K hint, honey send | **Home** (Chat composer reuses the send affordance only) |
|
||||
| 0.12 | `ActivityStream` | `os/warm/ActivityStream.tsx` (Chat composite) | `summary: string; durationMs?; steps: {tone; text: ReactNode; provenance?: {source; when?}}[]; defaultOpen?: boolean` — collapsible `--bg-2` card, violet spark, per-step `DotLive` + `ProvenanceLine` | **Chat** activity card |
|
||||
| 0.13 | `InlineApprovalCard` | `os/warm/InlineApprovalCard.tsx` | reuse `ApprovalRequest`; `onApprove; onDecline; alwaysAllow?` — `--honey-wash` bg, attention border, warning icon (inline, NOT a modal); shares risk vocab w/ `risk-display.tsx`/`ApprovalModal` `RISK_LABELS` | **Chat** approval card |
|
||||
|
||||
**Reuse as-is (do NOT rebuild — primitives.md §2):** `ConfidenceBadge`, `EvidenceChip`,
|
||||
`EvidencePanel`, `StatusBadge`, `ApprovalModal` (vocab source), `DetailDrawer`, all shadcn
|
||||
`ui/` primitives (tabs/card/button/badge/input/textarea/tooltip/popover/scroll-area),
|
||||
`Sidebar` active-state recipe, `command-catalog.ts` (⌘K vocabulary).
|
||||
|
||||
**Phase 0 footprint: ~13 new files (all in `os/warm/`), 0 modified.**
|
||||
Verify after Phase 0: `tsc -p apps/web/tsconfig.app.json` → 0; new atoms render in isolation
|
||||
(optional smoke test file). No screen wiring yet.
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase A — Home (Editorial)
|
||||
|
||||
**Primary file:** `apps/web/src/components/os/apps/HomeCockpit.tsx`. **Keep the component,
|
||||
its route (`HomeRoute.tsx:13`), props, `load()` (`:467-497`), the `connecting` race guard
|
||||
(`:499-507`), and all five render states.** Restyle/replace **only the normal-render body**
|
||||
(`HomeCockpit.tsx:580`, `data-testid="home-cockpit"`) and the sub-state shells.
|
||||
|
||||
### A.1 Section build order (top → bottom, home.md §4)
|
||||
1. **Container** — swap `max-w-4xl mx-auto p-6` (`:580`) → centered
|
||||
`max-w-[920px] mx-auto px-8 pt-[46px] pb-20`. Optional `.comb` honeycomb layer behind.
|
||||
2. **Greeting** (rework `GreetingHeader` `:136-159`): mono date row (`--honey`, uppercase,
|
||||
`.1em` tracking) with leading `DotLive tone="healthy"` + right-aligned `StreakChip`;
|
||||
two-line H1 (Hanken 600, `clamp(34px,5vw,52px)`) with the keyword honey-spanned. **Keep
|
||||
the human-date rendering** (`formatBriefingDate` semantics, test 6).
|
||||
3. **J08 review banner** (`:601-625`) — **KEEP intact** (testid `home-cockpit-review-banner`,
|
||||
cta `home-cockpit-review-cta`, event `{appId:'memory',filter:'unreviewed'}`); may restyle
|
||||
to a warm attention row. Test-locked (test 2/3).
|
||||
4. **`OvernightHero`** (new, replaces `OvernightPanel` `:230-288`): eyebrow "While you slept"
|
||||
+ composed story sentence + ≤4 `RunChip`s. **Must render when overnight is null/empty**
|
||||
(the test stubs `getHomeOvernight → null`) — degrade to a quiet "Nothing ran overnight"
|
||||
line, never crash.
|
||||
5. **"Pick up where you left off"** (restyle `RecentWorkspacesPanel` `:162-227`): change head
|
||||
copy "You were working on" → **"Pick up where you left off"** via `SectionLabel`; 2-col
|
||||
grid of warm `.ws-card` with `HexAvatar` (glyph from `name[0]`), title/time/summary,
|
||||
**"Continue →"**, optional status badge. **Keep `WorkspaceActionsMenu` kebab** (real
|
||||
G1 CRUD). Returns null at 0 cards.
|
||||
6. **"Waggle suggests"** (restyle `SuggestedActionsPanel` `:321-343`): head → **"Waggle
|
||||
suggests"**; pills → stacked `.move` rows (`IconTile` + title + composed sub-line +
|
||||
sliding arrow). Click → `onContinue(workspaceId, sessionId)`.
|
||||
7. **"Up next"** (`UpNextPanel` `:291-318`): **KEEP conditional** — testid
|
||||
`home-cockpit-upnext` present-when-items / absent-when-empty is test-locked (test 4/5).
|
||||
Restyle only.
|
||||
8. **`AskBar`** (new, replaces `QuickCapturePanel` `:346-434`): pill + honey "+" + ⌘K hint +
|
||||
honey send. Wire send → `onContinue`/new-chat; `+` keeps quick-capture (`adapter.quickCapture`)
|
||||
or opens ⌘K (`CommandCenter`).
|
||||
9. **Drop:** `ActiveModelsTile` (`:437-448`, never populated) and the segmented QuickCapture panel.
|
||||
|
||||
### A.2 Data wiring (home.md §2 — real vs graceful-mock)
|
||||
- **REAL (wire directly):** `briefing.greeting`, `briefing.userName` (also feeds the LOW
|
||||
user-row fix §0), `briefing.date` (humanize + add time), `recentWorkspaces[]`
|
||||
(name/group/summary/`formatRelative(lastActive)`/pendingCount), `suggestedActions[].label`,
|
||||
`upNext[]`, `overnight.{consolidated,artifactsCreated,automationsCompleted,failures}`,
|
||||
`needsReviewCount`.
|
||||
- **COMPOSED client-side (honest, from real fields):** overnight **story sentence** from
|
||||
counts + failures; run-chip labels for `"N memories consolidated"`/`"N artifacts created"`/
|
||||
`"N export failed"`; suggestion **sub-lines** from workspace name + relative time. Hex
|
||||
glyph from `name[0]`.
|
||||
- **MOCK — no data source anywhere (flag with `// TODO(backend): no data source yet`):**
|
||||
- 🔥 **streak** value (no `streak` field exists; habit-loop SCREENS §15 unimplemented).
|
||||
- "You're **ahead** of yesterday" second H1 line (no ahead-vs-yesterday signal).
|
||||
- "Teardown drafted · 9 competitors" first run chip + the "9 competitors" count (no
|
||||
per-run label field).
|
||||
- Workspace-card "agent live" badge (no per-card live flag) → derive or omit, don't fake.
|
||||
|
||||
### A.3 Exact copy (SCREENS §01 / home.html)
|
||||
- Date row e.g. `"Friday · June 14 · 8:42"` (compose from `briefing.date`); streak `"🔥 12-day streak"`.
|
||||
- H1 line 1 = `briefing.greeting` ("Good morning, Mara."); line 2 = "You're **ahead** of yesterday." (mock).
|
||||
- Eyebrow "While you slept"; story "Waggle finished the **Q2 competitor teardown**, folded
|
||||
**14 new memories** into the hive, and ran into **one snag** worth a look." (numbers from real counts).
|
||||
- Run chips: "Teardown drafted · 9 competitors" (healthy), "14 memories consolidated" (intel),
|
||||
"2 artifacts created" (work), "1 export failed" (risk).
|
||||
- Section heads "Pick up where you left off" / "Waggle suggests".
|
||||
- Ask-bar placeholder "Start something new — \"draft the board update from this week's work\"…".
|
||||
|
||||
### A.4 Test contract to keep green — `apps/web/src/test/p2-home-desktop.test.tsx`
|
||||
1. `home-cockpit` root testid present after load.
|
||||
2. `home-cockpit-review-banner` text "3 imported memories need your review" + `home-cockpit-review-cta`
|
||||
click dispatches `waggle:open-app {appId:'memory',filter:'unreviewed'}`.
|
||||
3. Banner omitted when `needsReviewCount` undefined.
|
||||
4. `home-cockpit-upnext` absent when `upNext` empty/undefined.
|
||||
5. `home-cockpit-upnext` contains item label when ≥1 item.
|
||||
6. Raw ISO must NOT appear; date humanized via `toLocaleDateString`.
|
||||
- Must render `home-cockpit` with **all-empty arrays** + `getHomeOvernight → null` (no crash).
|
||||
- Other testids (`home-cockpit-overnight/-suggested/-continue-*`, quickcapture) are **not**
|
||||
asserted → free to rename/remove.
|
||||
|
||||
### A.5 Files touched (Phase A)
|
||||
- **Modify:** `HomeCockpit.tsx` (normal-render body + sub-states). Optionally co-locate
|
||||
presentational subcomponents in a `home/` dir (CLAUDE.md "many small files") — at minimum
|
||||
`OvernightHero`, `AskBar`, `StreakChip` come from `os/warm/`.
|
||||
- **Touch (LOW fix #2):** Sidebar user-row wiring to pass `briefing.userName`.
|
||||
- **Verify:** `tsc -p apps/web/tsconfig.app.json` 0; `npm run test --root apps/web` →
|
||||
`p2-home-desktop.test.tsx` green; light ratchet green; live smoke (cold load + empty arrays
|
||||
+ null overnight render `home-cockpit`).
|
||||
|
||||
**Phase A footprint: ~1–4 files modified (HomeCockpit + optional `home/` subcomponents + Sidebar wiring), 0–3 created.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase B — Chat (split work-canvas)
|
||||
|
||||
**Primary file:** `apps/web/src/components/os/apps/ChatApp.tsx`. The thread, tool cards,
|
||||
artifact card, approval gate, model picker, persona pill, composer, and SSE/block reduction
|
||||
(`useChat.ts`) **all already exist and are design-compatible** — most of Phase B is RESTYLE
|
||||
+ token migration. Two things are net-new: the **right work canvas** and the **collapsible
|
||||
ActivityStream**.
|
||||
|
||||
### B.1 Where the canvas lives (chat.md §1 — critical)
|
||||
Append `<ChatWorkCanvas>` as a **sibling of ChatApp's chat column** inside ChatApp's existing
|
||||
flex root — root `<div className="flex h-full relative">` (`ChatApp.tsx:697`), chat column
|
||||
`<div className="flex flex-col flex-1 min-w-0">` (`:744`). This keeps the canvas **inside the
|
||||
kept-alive portal subtree** (`ChatHost.tsx`), so its state survives navigation for free.
|
||||
**Do NOT** add the canvas as a `WorkspaceDesktopApp` pane — that breaks the `chatSlot`/`activeTab`
|
||||
contract pinned by `p1a-chat-state.test.tsx` and lives outside keep-alive.
|
||||
|
||||
### B.2 Build order
|
||||
1. **Token migration** — Hive-DS classes (`bg-primary`, `text-emerald-400`, `bg-secondary`,
|
||||
`border-border`) → warm tokens (`--surface`, `--honey`, `--intel`, `--bg-2`, `--line-soft`).
|
||||
2. **Context header** (restyle `chat-header` `:746`): `HexAvatar` "C" + workspace name + mono
|
||||
sub "workspace · N memories · N sources" + right-side `ModelPill` (the current model picker
|
||||
`:931` restyled with a healthy `DotLive` + "auto ·" prefix) + Memory icon button (the
|
||||
current Memory chip `:815`). Autonomy/storage/team chips not in the design header → fold
|
||||
into the overflow menu via the existing `chat-header-layout.ts` decision.
|
||||
3. **Thread restyle** (`:1050-1191`):
|
||||
- User bubble (`:1108`): `bg-primary rounded-xl` → `--surface` bubble, asymmetric radius
|
||||
`4px 14px 14px 14px`, 15.5px/1.55.
|
||||
- Bot message: `HexAvatar` "W" + meta line "Waggle · {persona} · {model}"; honey `◆` bullets.
|
||||
4. **`ActivityStream`** (new, chat.md §5 #3): a grouping wrapper in `chat-blocks/` —
|
||||
`BlockRenderer` collapses a consecutive run of `step` (and optionally tool) blocks into one
|
||||
collapsible card: violet spark + "Worked across memory, web & files · N steps · Ns" header
|
||||
+ chevron. Derive step count + duration from the grouped blocks. **Default-open on the
|
||||
active turn** (track `isStreaming`/active turn), collapsed on prior turns. Per-step
|
||||
`DotLive` + `ProvenanceLine` **only when source data exists** — do NOT fabricate
|
||||
`⬡ mem://hive` (SSE `step` carries no source field today; flag to backend in §5).
|
||||
5. **`InlineApprovalCard`** (restyle `ApprovalGate` `:223`): `--honey-wash` bg + attention
|
||||
border + warning icon; copy "Approve before I leave your machine" + mono external target +
|
||||
"Approve & export"/"Not now". **Keep `RiskBadge` + the Always-allow gating logic.**
|
||||
6. **`ChatWorkCanvas`** (new `<aside>`): `--bg-2`, `--line-soft` left border, `width:42%`,
|
||||
`transition .25s`. Head: title (artifact basename) + healthy mono "● live draft" + "Open in
|
||||
Artifacts" icon button. Body = `.doc` rendered via the **existing markdown path** (reuse
|
||||
`TextBlock`'s renderer) from the **latest completed `write_file`/`edit_file` block's
|
||||
`input.content`/`result`** (reuse `ArtifactBlock.isArtifactBlock` `:21` + path logic).
|
||||
Blinking honey type-cursor (`.hex-cursor`) is **cosmetic, shown while `isStreaming`**. Thread
|
||||
`max-width` drops 760→~620px when open. `@media (max-width:820px)` → hide canvas (chat full width).
|
||||
Open/close state lives in ChatApp (inside keep-alive).
|
||||
7. **Composer restyle** (`:1194`): `:focus-within` → `--honey-line` + `--honey-glow`; placeholder
|
||||
"Reply, or ask Waggle to take the next step…"; chips row Attach / "Persona: {name}" / Tools;
|
||||
mono hint "⏎ send · ⌘K commands"; honey ↑ send (40px). Keep the slash menu + send wiring.
|
||||
|
||||
### B.3 Data wiring (chat.md §2)
|
||||
- **REAL (no hook change):** the entire `useChat` stream/`blocks[]` reduction; tool cards;
|
||||
artifact card + Open-in-Files; feedback; slash menu; pins; persona; autonomy; keep-alive.
|
||||
- **DERIVED:** canvas doc body from the latest file-write block; ActivityStream summary
|
||||
(count/duration) from grouped blocks; ModelPill "auto" when model is default/unset.
|
||||
- **MISSING (don't fabricate):** structured per-step `source`/provenance (no SSE field) →
|
||||
render the provenance pill only when present; canvas "live document body" stream (today only
|
||||
`input.path` + opaque `result`) → PR3 shows the **last completed artifact's content**, "live
|
||||
drafting" cursor is cosmetic.
|
||||
|
||||
### B.4 Test contract to keep green
|
||||
- **`chat-artifact-block.test.tsx`** — keep `data-testid="chat-artifact-block"` +
|
||||
`chat-artifact-open`, Created/Updated copy, and the `isArtifactBlock` routing predicate. **The
|
||||
canvas is ADDITIVE — the inline artifact card stays.**
|
||||
- **`p1a-chat-state.test.tsx`** — do NOT change `WorkspaceDesktopApp`'s `chatSlot` / `activeTab` /
|
||||
`onTabChange` contract (test ids `ws-tab-chat`, `ws-tab-tasks`, `ws-tasks-tab`, `chat-slot-stub`,
|
||||
`ws-chat-tab-open`-absent-when-slot). The canvas lives inside the slot subtree, not as a new
|
||||
desktop prop. Persona/autonomy persistence (`waggle-chat-state-v1`) unchanged.
|
||||
- **`chat-blocks/TextBlock.test.tsx`**, **`context-rail-fetch.test.ts`** (the `onContextRail`
|
||||
double-click path `ChatApp.tsx:1095`) — keep behavior; grep before editing.
|
||||
|
||||
### B.5 Files touched (Phase B)
|
||||
- **Modify:** `ChatApp.tsx` (header/thread/composer restyle + canvas sibling + open state);
|
||||
`chat-blocks/BlockRenderer.tsx` (group steps into `ActivityStream`); `chat-blocks/StepBlock.tsx`
|
||||
(feed into ActivityStream); `ChatWindowInstance.tsx` (only if canvas needs a model/"auto" prop —
|
||||
minimal).
|
||||
- **Create:** `os/warm/ChatWorkCanvas.tsx` (or `chat-blocks/`), `os/warm/ActivityStream.tsx`,
|
||||
`os/warm/InlineApprovalCard.tsx`, `os/warm/ModelPill.tsx` (from Phase 0), a small canvas-content
|
||||
selector helper.
|
||||
- **Verify:** `tsc -p apps/web/tsconfig.app.json` 0; FE vitest green incl. `chat-artifact-block.test.tsx`
|
||||
+ `p1a-chat-state.test.tsx` + `chat-blocks/TextBlock.test.tsx`; live smoke (send a message →
|
||||
activity card default-open → file-write opens canvas → approval card renders → navigate away and
|
||||
back, canvas state survives).
|
||||
|
||||
**Phase B footprint: ~4 files modified, ~4–5 files created.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase C — Workspace (Overview + tabs)
|
||||
|
||||
**Primary files:** `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx` (1020 LOC) +
|
||||
`apps/web/src/routes/WorkspaceRoute.tsx`. Rebuild the **shell header + tab bar + Overview**;
|
||||
the other 5 tabs embed/deep-link the **real existing screens** (workspace.md §6).
|
||||
|
||||
### C.1 Build order
|
||||
1. **Tab set 8 → 6** (workspace.md G1): drop Research/Tasks/Timeline/Settings from the bar;
|
||||
final set **Overview · Chat · Memory · Artifacts · Files · Team**. Update `WorkspaceTabId`
|
||||
union + `TABS` (`WorkspaceDesktopApp.tsx:58-77`) and `WS_TABS` (`WorkspaceRoute.tsx:20-22`).
|
||||
- **Do NOT delete `<TasksTab>` the component** — relocate Tasks into the Overview "Up next"
|
||||
card; keep it reachable. Unknown `:tab?` already falls back to Overview
|
||||
(`WorkspaceRoute.tsx:43-45`), so stale `/workspaces/:id/tasks` links degrade gracefully.
|
||||
- **Files/Team** are NEW tabs → embed the existing Files surface (`getWorkspaceFiles`,
|
||||
already loaded) and Team (global roster today — documented TODO).
|
||||
2. **Header** (restyle `:759-811`): breadcrumb `Home › {name}` (mono, `--text-dim`); 46px
|
||||
`HexAvatar`; H1 (Hanken 650, 28px); meta row `DotLive`+"1 agent live · N memories · N sources
|
||||
· updated Xago"; right actions = **Memory** (ghost) + **Continue →** (honey). **Keep
|
||||
`WorkspaceActionsMenu` kebab** (real CRUD, not in mock).
|
||||
3. **Tab bar** (restyle `:814-839`): honey 2px bottom-border on active (honey already comes via
|
||||
`--primary`); add per-tab mono count `.cnt` span. Keep `ws-tab-bar`, `ws-tab-<id>` testids.
|
||||
4. **Overview re-layout** (workspace.md G7): replace the 3-col widget grid (`OverviewTab`
|
||||
`:518-545`) with a **2-col grid `1.7fr / 1fr`, gap 22px**:
|
||||
- **Left col:** summary card (honey-bold keywords, from `ctx.summary`); **"What Waggle
|
||||
knows"** (`SectionLabel` + `HexCheckTile` fact rows + `ProvenanceLine`); **"Recent work"**
|
||||
(`IconTile` ext rows + name + sub + mono time + hover honey border).
|
||||
- **Right col (folds the old `w-72` aside in — G13):** **Status card** (agent/model/memories+delta/
|
||||
needs-review); **Up next** (derived from pending/blocked/nextActions/schedules); **Team card**
|
||||
(avatar rows + roles, global roster).
|
||||
5. **Memory tab** stays `<MemoryCenterTab mind="workspace">` (`:937-941`) — **already correct, keep**.
|
||||
|
||||
### C.2 Data wiring (workspace.md §2)
|
||||
- **REAL:** `ctx.summary`, `ctx.recentMemories`/`recentDecisions` (content + date), `ctx.stats`
|
||||
(memoryCount/sessionCount/fileCount → tab counts), `getWorkspaceFiles` (recent-work names + times),
|
||||
`useRoomState` live ("1 agent live"), `ctx.workspace.model`, `getWorkspaceActivity`,
|
||||
`getTeamMembers` (global), `state.{pending,blocked,nextActions}` + cron schedules (Up next).
|
||||
`relativeTime` (`:135-147`) + `initialsOf` (`:128-133`) helpers exist.
|
||||
- **DERIVED:** "updated Xago" from `ctx.lastActive`; ext-tile from filename; tab counts from stats.
|
||||
- **MOCK / data-gap (render only where real; flag, don't fabricate):**
|
||||
- **Per-fact / per-artifact provenance `⬡ source · when`** — frames HAVE a `source` column
|
||||
server-side (`workspaces.ts:291` writes, SELECTs around `:413`) but it is **not projected
|
||||
into `recentMemories`/`recentDecisions`** today. PR3 renders `ProvenanceLine` **with `when`
|
||||
(real date) and source only if projected**; otherwise show date-only and leave a
|
||||
`// TODO(backend): project frame.source` note. (This is the keystone PR3.5 hook — §0.)
|
||||
- "9 sources" (no `sourceCount`), "+6 today" delta, per-workspace "N to review", a distinct
|
||||
**artifacts** entity vs files, agent NAME on the live row → mock/omit, flagged.
|
||||
|
||||
### C.3 Exact copy (SCREENS §03 / workspace.html) — sample/derive
|
||||
Summary, fact rows ("Mem0 is cloud-only…", prov `web · mem0.ai · 2h ago`), recent-work
|
||||
(`teardown.md` "Q2 competitive teardown · 9 competitors · 2h ago"), Status
|
||||
(`● Research-synth · live` / `auto · Claude Sonnet` / `142 +6 today` / `3 memories`), Up next
|
||||
(`Board brief from teardown` draft / `Export table → Salesforce` awaiting you / `Weekly digest` 17:00),
|
||||
Team (`Mara K. · Owner`, `Research-synth · Agent · live`). Use **real** fields where present;
|
||||
sample strings are mock-until-backed.
|
||||
|
||||
### C.4 Test contract to keep green
|
||||
- **`p1a-workspace-route.test.tsx`** — route↔shell contract: `selectWorkspace(routedId)` on
|
||||
deep-link / Back-Forward, skip-when-active, never sync `local-default`, `path="workspaces/:workspaceId/:tab?"`.
|
||||
The `:tab?` param + the `selectWorkspace` effect + the `local-default` guard must survive the rewrite.
|
||||
- **`lib/workspace-briefing-state.test.ts`** — briefing-collapsed localStorage helpers (key prefix
|
||||
`waggle:workspace-briefing-collapsed:`); only relevant if briefing-collapse is carried into the new
|
||||
Overview; helpers reusable as-is.
|
||||
- **Implicit testid contract (Playwright/smoke):** keep `ws-desktop-root`, `ws-tab-bar`, `ws-tab-<id>`,
|
||||
`ws-tab-panel`, `ws-overview-grid`, `ws-info-panel`, `ws-status-pill`, `ws-members-stack`,
|
||||
`ws-agents-running`, `ws-desktop-{loading,permission-denied,notfound,offline,retry}`, `ws-memory-tab`.
|
||||
Dropping Research/Tasks/Timeline/Settings removes `ws-tab-{research,tasks,timeline,settings}` — none
|
||||
appear in the two unit tests, but **grep untracked Playwright smokes before deleting**.
|
||||
- **No test pins the 8-tab set, widget-grid, or right-panel** → the re-layout + tab reduction are free.
|
||||
|
||||
### C.5 Files touched (Phase C)
|
||||
- **Modify:** `WorkspaceDesktopApp.tsx` (header + tab bar + Overview 2-col + tab set, fold aside in),
|
||||
`WorkspaceRoute.tsx` (`WS_TABS` 6-tab set + union). Optionally `os/apps/workspace/` subcomponents.
|
||||
- **Create:** `os/warm/HexCheckTile.tsx` + `ProvenanceLine.tsx` (Phase 0); `FactRow`, `ArtRow`,
|
||||
`StatusCard`, `UpNextCard`, `TeamCard` (co-located `workspace/` or inline if small).
|
||||
- **Verify:** `tsc -p apps/web/tsconfig.app.json` 0; FE vitest green incl. `p1a-workspace-route.test.tsx`
|
||||
+ `workspace-briefing-state.test.ts`; live smoke (deep-link a tab, Back/Forward, Overview renders with
|
||||
real ctx, tabs route to real screens, empty-workspace state still renders).
|
||||
|
||||
**Phase C footprint: ~2 files modified, ~5–7 files created.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Risks + open questions (need founder/data decision OR graceful degradation)
|
||||
|
||||
| Topic | Decision / risk | PR3 default (degrade gracefully) |
|
||||
|---|---|---|
|
||||
| **Overnight story = mocked vs real** | Server gives raw counts, no narrative or per-run labels (home.md §2). The hero sentence + "9 competitors" chip are composed/mock. | Compose the sentence from real counts/failures; flag the "9 competitors" chip `// TODO(backend)`. When overnight is null/empty → quiet "Nothing ran overnight" line, still render `home-cockpit`. |
|
||||
| **Streak source** | No `streak` field anywhere (home.md §2; SCREENS §15 unimplemented). | Render `StreakChip` with a **constant mock value + explicit TODO**, or hide behind a flag until a backend field lands. Do not fake daily logic. |
|
||||
| **"ahead of yesterday"** | No ahead-vs-yesterday signal. | Render as static copy with TODO, or drop the second H1 line if founder prefers no mock. |
|
||||
| **Chat canvas content stream** | No live-document stream — only file-write `input.content`/opaque `result` (chat.md §2/§5 #2). | Canvas shows the **last completed artifact's content**; "live draft" cursor is cosmetic during `isStreaming`. Flag the need for a doc-body stream channel to backend. |
|
||||
| **Provenance `⬡ source` (Chat steps + Workspace facts)** | Frame `source` exists server-side but is **not projected** into `recentMemories`/`recentDecisions`/steps (workspace.md §2; chat.md §2). This is the core trust pattern (README §6) **and** the PR3.5 keystone. | Render `ProvenanceLine` with **real `when` (date)** always; show source **only when projected** — otherwise date-only + `// TODO(backend): project frame.source`. **A tiny server change to project `source` would unlock this for both screens — recommend founder greenlight a 1-field projection in PR3 or PR3.5.** |
|
||||
| **Workspace data gaps** | "9 sources", "+6 today", per-workspace "N to review", distinct artifacts entity, agent NAME on live row — all absent. | Omit or mock-with-TODO; never block render. |
|
||||
| **Team roster is global, not per-workspace** | `getTeamMembers()` returns global roster (documented TODO `WorkspaceDesktopApp.tsx:660-662`). | Render global roster as today; note the scope gap. |
|
||||
| **`continueSessionId` omitted server-side** | "Continue" lands at chat root, not a session (home.md §2). | Continue → chat root; acceptable, pre-existing. |
|
||||
| **LOW fix — Chat no-workspace** | Needs `hasRealActiveWorkspace` signal in the spine builder. | Route Chat → switcher/Home when no real workspace; dim the item. Confirm the selector source. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Verification gate (run per phase + final)
|
||||
|
||||
**Type + tests (cite exact targets):**
|
||||
- `npx tsc -p apps/web/tsconfig.app.json` → **0 errors** (this is the FE typecheck; the
|
||||
`npm run -w apps/web typecheck` script is a silent no-op — solution tsconfig).
|
||||
- FE vitest from `apps/web` root, **green**, specifically:
|
||||
- Home: `apps/web/src/test/p2-home-desktop.test.tsx`
|
||||
- Chat: `apps/web/src/test/chat-artifact-block.test.tsx`, `apps/web/src/test/p1a-chat-state.test.tsx`,
|
||||
`apps/web/src/components/os/apps/chat-blocks/TextBlock.test.tsx`, `apps/web/src/test/context-rail-fetch.test.ts`
|
||||
- Workspace: `apps/web/src/test/p1a-workspace-route.test.tsx`, `apps/web/src/lib/workspace-briefing-state.test.ts`
|
||||
- Theme ratchet (all screens): `apps/web/src/test/light-mode-tokens.test.ts`
|
||||
- `npx eslint .` run from **`apps/web`** cwd → clean.
|
||||
|
||||
**Live smoke checklist (per screen, dark + light toggle):**
|
||||
- **Home:** cold-load renders `home-cockpit`; greeting + streak + hero render; **empty arrays +
|
||||
null overnight still render** (no crash); J08 banner CTA fires `waggle:open-app`; ask-bar sends;
|
||||
user-row shows `userName`.
|
||||
- **Chat:** send a message → ActivityStream default-open on active turn, collapses on prior; a
|
||||
file-write opens the work canvas; canvas hides < 820px; approval card renders honey-wash with
|
||||
working Allow/Deny; **navigate away and back — canvas + chat state survive** (keep-alive); inline
|
||||
artifact card + Open-in-Files still work.
|
||||
- **Workspace:** deep-link a tab + Back/Forward (route↔shell intact); Overview 2-col renders with
|
||||
real ctx; fact rows show provenance `when`; tabs route to the real Chat/Memory/Artifacts/Files/Team
|
||||
screens; empty-workspace + error states still render.
|
||||
|
||||
**Definition of done:** tsc 0, all cited FE tests green, eslint clean, live smoke passes on all
|
||||
three screens in both themes, the two PR1 LOW items cleared, no fabricated data shipped (mocks
|
||||
carry `// TODO(backend)`), PR3.5 hooks left (clickable provenance/fact rows, intact J08 banner).
|
||||
349
docs/redesign-warm-hive/PR35-BUILD-PLAN.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Warm-Hive PR3.5 — Memory-Trust Build Plan
|
||||
|
||||
> Synthesized from the 5 recon files in `docs/redesign-warm-hive/pr35-recon/` (01–05),
|
||||
> all verified on branch `feature/warm-hive-pr3` @ `dac7b696`, 2026-06-16.
|
||||
> This plan has two halves: **Part 1** = the `frame.source` projection (the keystone
|
||||
> enabler), **Part 2** = the screen-19 Memory-Trust UI, built in phases on top of it.
|
||||
>
|
||||
> Repo root: `D:/Projects/waggle-os`. Substrate: `packages/hive-mind-core/src/mind/`.
|
||||
> Server: `packages/server/src/local/routes/`. UI: `apps/web/src/`.
|
||||
>
|
||||
> **No-fabrication contract (inherited from PR3):** confidence, freshness, and trace-reason
|
||||
> are the three places this PR could silently invent data. The plan gates each one off
|
||||
> (PR3 streak pattern) wherever the substrate does not hold a real value. See §3 + §5.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — `frame.source` projection (the keystone enabler)
|
||||
|
||||
The ⬡ provenance pill on Chat + Workspace is blocked by one missing column in two read
|
||||
paths. Recon 01 maps the FE-facing path (the `/context` route → `WorkspaceBriefing`); recon
|
||||
02 maps the SSE step path. The DDL column already exists and is NOT NULL DEFAULT — **no
|
||||
migration, no DDL change, no backfill.** This is a pure read-side projection.
|
||||
|
||||
### 1.1 The column (no change — context only)
|
||||
|
||||
`packages/hive-mind-core/src/mind/schema.ts:56-57` —
|
||||
`source TEXT NOT NULL DEFAULT 'user_stated' CHECK (source IN ('user_stated','tool_verified','agent_inferred','import','system'))`.
|
||||
Persisted rows only ever hold these **5** values (the TS `FrameSource` union at
|
||||
`frames.ts:25` is wider — `personal|workspace|team_sync` are search-time mind labels that
|
||||
fail the CHECK and never persist). The FE owns the label/icon map for the 5 values; the
|
||||
route returns the raw string.
|
||||
|
||||
### 1.2 Workspace Briefing path — REQUIRED (the FE pill keystone)
|
||||
|
||||
**File A — `packages/server/src/local/routes/workspaces.ts`** (`GET /api/workspaces/:id/context`, handler @ `:364`)
|
||||
|
||||
Ordered edits:
|
||||
|
||||
1. **`:376`** — widen the `let recentMemories` local from
|
||||
`Array<{ content: string; importance: string; date: string }>` →
|
||||
`Array<{ content: string; importance: string; source: string; date: string }>`.
|
||||
2. **`:377`** — widen the `let recentDecisions` local from
|
||||
`Array<{ content: string; date: string }>` →
|
||||
`Array<{ content: string; source: string; date: string }>`.
|
||||
*(Skipping 1+2 makes the server `tsc` fail against the narrower locals — and `npm run build` does NOT typecheck the sidecar, so this only surfaces under the §1.4 server tsc.)*
|
||||
3. **`:393-400`** — `recentMemories` SELECT: add `source` to the column list and to the
|
||||
`as Array<{...}>` cast → `SELECT content, importance, source, created_at FROM memory_frames …`.
|
||||
4. **`:402-406`** — `recentMemories` `.map()` projection: add `source: f.source,`.
|
||||
5. **`:409-417`** — `recentDecisions` SELECT: add `source` to the column list and cast →
|
||||
`SELECT content, source, created_at FROM memory_frames …`.
|
||||
6. **`:419-429`** — `recentDecisions` `.map()` returned object: add `source: f.source,`.
|
||||
|
||||
(Exact before/after blocks are in recon 01 §2a/§2b — copy them verbatim.) Purely additive;
|
||||
no existing consumer breaks.
|
||||
|
||||
**File B — `apps/web/src/lib/types.ts`** (interface `WorkspaceContext`, `:248-249`)
|
||||
|
||||
7. Widen both inline item shapes with an **optional** `source?`:
|
||||
- `recentDecisions?: Array<{ content: string; source?: string; date: string }>;`
|
||||
- `recentMemories?: Array<{ content: string; importance: string; source?: string; date: string }>;`
|
||||
- Keep `source` optional so pre-PR3.5 sidecars still typecheck and the FE degrades to
|
||||
"no pill" when absent.
|
||||
|
||||
**Consumer (no type change, render only) — `apps/web/src/components/os/WorkspaceBriefing.tsx`**
|
||||
|
||||
8. `recentMemories` rendered at `:186-197` (importance badge `:189-193`) and `recentDecisions`
|
||||
at `:164-178`. Swap the bare mono date `<div>` for
|
||||
`<ProvenanceLine source={m.source} when={m.date} onClick={…} />` **only when `m.source` is
|
||||
present**; otherwise keep PR3's bare date `<div>` (the date-only fallback — never a
|
||||
fabricated source). This is the same affordance PR3 already left hooked.
|
||||
|
||||
**DO NOT TOUCH — the parallel system-prompt builder.** `workspace-context.ts` /
|
||||
`workspace-state.ts` (`buildWorkspaceState` → `WorkspaceNowBlock`) feeds the **system
|
||||
prompt**, not the FE pill, and its `StateItem.source` is a *different* axis
|
||||
(`memory|session|awareness`). Adding `frame.source` there is out of scope and would collide
|
||||
on the `source` name (recon 01 §4). *(Note: recon 05 §7 names `workspace-context.ts:283-284`
|
||||
as "the" projection gap — that is the system-prompt path; the FE pill keystone is the
|
||||
`workspaces.ts` `/context` route in recon 01. They are different routes; for PR3.5 the FE
|
||||
pill, do File A above, not `workspace-context.ts`.)*
|
||||
|
||||
### 1.3 SSE step path — DECISION: **project on `auto_recall` only, paired with the recallMemory widen** (recon 02)
|
||||
|
||||
Recon 02 verdict: **NEEDS-WIRING, not trivially projectable.** A streamed `step` carries
|
||||
`{ content: string }` only; the agent loop exposes `onToolUse/onToolResult(name,input,result)`
|
||||
with no frame/source; the real `source` is destroyed when `recallMemory()` flattens frames to
|
||||
`string[]` text (`orchestrator.ts:453-457`).
|
||||
|
||||
**Project-or-defer decision:**
|
||||
- **Blanket "source on every step" is WRONG by design** — generic steps (bash, drafting,
|
||||
budget) have no provenance; stamping them is fabrication (PR3's refused move).
|
||||
- **Only the memory-recall step (`auto_recall`) has genuine provenance.** Wiring it is
|
||||
in-scope for PR3.5 **iff paired with the `recallMemory` return-shape widen**. If we want to
|
||||
stay server-light this arc, **DEFER** the SSE step pill and keep PR3's "render the affordance,
|
||||
never fabricated provenance" stance — the Chat ActivityStream already gates on
|
||||
`s.provenance` presence, so nothing breaks.
|
||||
|
||||
**Recommended for PR3.5: do the `auto_recall` slice** (it is the honest, load-bearing one and
|
||||
the same "frame.source 1-field projection" the S2 handoff named). The 5 edits / 4 files:
|
||||
|
||||
1. **`packages/agent/src/orchestrator.ts:453-457`** (load-bearing) — widen `recallMemory()`
|
||||
return to carry per-snippet provenance, e.g. add
|
||||
`recalledFrames?: Array<{ text: string; source: FrameSource; sourceUrl?: string; when?: string }>`
|
||||
alongside the existing `recalled: string[]`. Frame objects with `.source` are already in
|
||||
hand inside `recallMemory` — this is "stop flattening it", not a new query.
|
||||
2. **`packages/server/src/local/routes/chat.ts:780-781`** — emit `source`/`provenance` on the
|
||||
`auto_recall` step or its `tool_result` (`{ content, provenance?: { source, when } }` is
|
||||
additive, backward-compatible).
|
||||
3. **`apps/web/src/lib/types.ts:474-479`** — add optional `provenance?: { source: string; when?: string }`
|
||||
to `StepContentBlock` (and tolerate it in `StreamEvent.data` handling, `:616-619`).
|
||||
4. **`apps/web/src/hooks/useChat.ts:152`** — carry `data.provenance` onto the pushed step block.
|
||||
5. **`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:30-33`** — project
|
||||
`s.provenance` into `ActivityStep` (the `ActivityStream`/`ProvenanceLine` path already
|
||||
gates on presence; remove only the part of the omitted-provenance comment that no longer
|
||||
holds). Leave all non-memory steps provenance-less by design.
|
||||
|
||||
### 1.4 Verification (Part 1)
|
||||
|
||||
```bash
|
||||
# server route typecheck — REQUIRED; npm run build skips the sidecar (runs via tsx transpile-only)
|
||||
npx tsc --noEmit --project packages/server/tsconfig.json
|
||||
# agent (only if §1.3 done — recallMemory widen)
|
||||
npx tsc --noEmit --project packages/agent/tsconfig.json
|
||||
# FE typecheck — use the app tsconfig directly (the workspace alias is a silent no-op)
|
||||
npx tsc --noEmit -p apps/web/tsconfig.app.json
|
||||
npm run test -- --run # FE unit + touched server suites
|
||||
npm run lint
|
||||
```
|
||||
Live check: open a workspace with seeded memories → Chat + Workspace Overview render the ⬡
|
||||
pill on rows that have a `source`; rows without a source show the bare date (no fabricated
|
||||
pill). Console: 0 errors.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Memory-Trust (screen 19), phased
|
||||
|
||||
Target screen: `docs/design_handoff_waggle_app/design-files/screens/memory-trust.html`
|
||||
(authoritative mock, 1:1 token match to PR3's warm tokens per recon 04 §8 — no remapping).
|
||||
Two view modes behind a segmented control: **A "Manage memory"** (default) and **B "Why did
|
||||
you do that?"**. The standalone host is `MemoryCenterApp.tsx` (screen 19 extends/replaces it);
|
||||
the reusable core `memory/MemoryCenterTab.tsx` is shared by the Workspace Memory tab — change
|
||||
once, both update (recon 03).
|
||||
|
||||
> **Reuse, don't rebuild:** `ConfidenceBadge`, `EvidenceChip`, `DetailDrawer`, `EvidencePanel`
|
||||
> already exist in `components/ui/` (commit `afe96355`, pre-PR3) and ship today via Memory
|
||||
> Center. The new warm atoms are only the 4 listed in §4.
|
||||
|
||||
### Phase 0 — Primitives (new warm atoms + token decision)
|
||||
|
||||
**Files to create** (`apps/web/src/components/os/warm/`, export from `warm/index.ts:7-20`):
|
||||
- `ConfidenceRing.tsx` — the §5a 38px ring (see §4).
|
||||
- `ConfidenceBadge` decision: the existing `ui/confidence-badge.tsx` uses the Hive DS
|
||||
`--sem-*` family; the warm screen wants `--healthy/--attention/--risk`. **Token-family
|
||||
decision (founder call, see §5):** align new Memory-Trust confidence atoms to the **warm
|
||||
`--sem-*`→band** mapping the screen 19 mock uses (green ≥85 / honey ≥60 / terracotta <60),
|
||||
OR reuse `ui/ConfidenceBadge` if its `--sem-*` bands render acceptably. Default
|
||||
recommendation: build `ConfidenceRing` against warm tokens (matches the mock exactly),
|
||||
reuse `ui/ConfidenceBadge` where a pill (not a ring) is wanted.
|
||||
- `EvidenceChip` for the trace: reuse `ui/evidence-chip.tsx` (recon 03). Only build a thin
|
||||
`TraceEvidenceChip` wrapper if the §6b `.ev` chip needs the multi-segment
|
||||
`confidence X% · source · when · still fresh` format.
|
||||
- `DetailDrawer`: reuse `ui/detail-drawer.tsx` (recon 03) for the memory-detail open.
|
||||
|
||||
**Files to modify:** `ProvenanceLine.tsx` — extend to optionally render the §5b 3-segment
|
||||
form `⬡ <id> · source: <src> · ● <freshness>` (add a `source:`-prefix variant + a `●`
|
||||
freshness segment colored healthy/attention). Today it renders `⬡ source · when` only.
|
||||
|
||||
**Tokens/primitives reused:** all `--honey/--healthy/--attention/--risk/--intel` + washes +
|
||||
`--r-lg 18px`/`--r-xl 26px` + JetBrains Mono/Hanken already defined in both themes
|
||||
(`index.css:162-168` dark / `:298-304` light). `HexAvatar`, `SectionLabel`, `DotLive`,
|
||||
`RunChip`/chip patterns already exist.
|
||||
|
||||
**Data:** none (pure presentational atoms).
|
||||
|
||||
### Phase A — Header / segmented control + editorial hero + stat bar
|
||||
|
||||
**Files to create/modify:**
|
||||
- New `MemoryTrustApp.tsx` (or extend `MemoryCenterApp.tsx`) hosting the §1 sticky `.controls`
|
||||
bar: eyebrow `Memory Trust · view`, 2-button segmented switch (`Manage memory` default-on
|
||||
honey-fill `#1a1407` ink | `Why did you do that?`), swapping `.vlabel`, theme toggle.
|
||||
- §2 editorial hero per view (honey eyebrow + 28px/650 H1 with honey non-italic `<em>` accent
|
||||
+ 64ch body with bold `--text-2` spans). Verbatim copy in recon 04 §2.
|
||||
- §3 stat bar (View A): 4 cards `repeat(4,1fr)`→2-col @820px.
|
||||
|
||||
**Primitives/tokens:** `SectionLabel` for eyebrows; honey-fill segment = `.seg button.on`
|
||||
treatment (`background:var(--honey); color:#1a1407`); warn cards use the
|
||||
`--attention`/`color-mix` border per recon 04 §3.
|
||||
|
||||
**Data — REAL vs route-needed:**
|
||||
- Stat #1 "Memories in this hive" (`142`) — **REAL**, from `GET /api/memory/stats` (counts
|
||||
only, mind-isolation honored).
|
||||
- Stat #2 "High confidence & fresh" (`128`) — **DERIVABLE/PARTIAL**: confidence is sparse
|
||||
(harvest-only), freshness is derivable from `created_at`. **If confidence is absent for the
|
||||
mind, gate this card off** (show "—" or hide), do not invent a count. See §3.
|
||||
- Stat #3 "Stale · worth a review" (`9`) — **DERIVABLE** from `created_at` via
|
||||
`computeTemporalScore` (FE compute or thin route).
|
||||
- Stat #4 "Awaiting your confirm" (`3`) — **needs route**: `GET /api/memory?status=unreviewed`
|
||||
(filter already works, `memory-center.ts:198`) — but nothing *writes* `unreviewed` today, so
|
||||
the count may legitimately be 0. Wire to the real query; do not seed a fake number.
|
||||
|
||||
### Phase B — Search + filters (View A toolbar)
|
||||
|
||||
**Files to modify:** the §4 `.toolbar` inside `MemoryTrustApp` — search input (placeholder
|
||||
`Search what Waggle knows… or ask it to forget something`) + 4 filter chips (`All` on /
|
||||
`Stale` / `Needs confirm` / `Forgotten`).
|
||||
|
||||
**Primitives/tokens:** active chip = `--honey-wash` fill + `--honey-line` border (recon 04 §4);
|
||||
reuse warm chip pattern.
|
||||
|
||||
**Data — REAL vs route-needed:**
|
||||
- Search → `GET /api/memory?mind&q&kind&status&minConfidence&limit` (`memory-center.ts:167`) —
|
||||
**REAL**.
|
||||
- `Stale` filter → **DERIVABLE** client-side over returned `created_at`.
|
||||
- `Needs confirm` filter → `GET /api/memory?status=unreviewed` — **REAL query** (may return 0).
|
||||
- `Forgotten` filter → **no tombstone exists** (forget is a hard delete, `frames.ts:321`).
|
||||
**Gate this chip off** or relabel — there is no "forgotten" list to show. See §3.
|
||||
|
||||
### Phase C — Memory rows (confidence ring + provenance + forget/correct/confirm/stale)
|
||||
|
||||
**Files to create/modify:** the §5 `.mem` row component (3-col `[conf ring] [body] [actions]`),
|
||||
rendered into `.mems` grid. Reuse the `memory/MemoryCenterTab.tsx` + `MemoryCard.tsx` pattern
|
||||
(already consumes `DetailDrawer`/`ConfidenceBadge`/`EvidencePanel`); add the warm row treatment.
|
||||
|
||||
**Primitives/tokens:** new `ConfidenceRing` (§4); extended `ProvenanceLine` (3-segment §5b);
|
||||
corrected banner = `--healthy-wash`; stale banner = `--honey-wash`+`--honey-line`; disputed =
|
||||
`--risk` border + strikethrough; danger-forget hover = `--risk` (recon 04 §5).
|
||||
|
||||
**Data — REAL vs route-needed:**
|
||||
- Row content / id / source → **REAL** (from `/api/memory` `Memory` shape).
|
||||
- **Forget** button → `DELETE /api/memory/:id?mind=…` — **REAL, no new backend** (hard delete +
|
||||
audit event). Row animates out then removed.
|
||||
- **Correct / edit** button → `PATCH /api/memory/:id` (content + reclassify) — **REAL, no new
|
||||
backend**.
|
||||
- **Confidence ring** → `Memory.confidence`, **only present on harvested frames**. **Hide the
|
||||
ring (or show a neutral "—" state) when `confidence` is undefined** — never default a number.
|
||||
See §3.
|
||||
- **Freshness `●`** → **DERIVABLE** from `created_at` (`computeTemporalScore`, 7-day boost /
|
||||
30-day half-life). Present as honest age ("aging — last seen 6w ago"), never a stored %.
|
||||
- **Stale banner / "Still true?"** → derivable trigger; **Confirm** action → **MUST-BUILD
|
||||
route** `POST /api/memory/:id/confirm` (set `metadata.status='active'`, mirroring
|
||||
`/archive` at `memory-center.ts:355`). Cheap; the `MemoryStatus` rails exist.
|
||||
|
||||
### Phase D — "Why did you do that?" trace (View B)
|
||||
|
||||
**Files to create/modify:** the §6 `.trace` card — `HexAvatar` header (`trace #a1f9`), 4-node
|
||||
connector chain (Goal→Recalled 3→Cross-checked→Acted; dots intel/honey/honey/healthy; evidence
|
||||
chip `confidence 94% · source: chat · Tue · still fresh`), 3-button footer (Looks right / That
|
||||
memory is wrong → correct it / Forget #M-204 & redo). Shared trust-principle footnote + green
|
||||
slide-up toast on both views.
|
||||
|
||||
**Primitives/tokens:** `HexAvatar`, `DotLive`, reused `EvidenceChip`; connector line per §6b;
|
||||
footer go-button honey-fill / ghost / danger per §6c.
|
||||
|
||||
**Data — REAL vs route-needed (the honest hard part):**
|
||||
- Trace store is **rich and populated for chat** (`execution_traces` + `TraceRecorder` per
|
||||
turn, `chat.ts:1277-1424`): reasoning[], toolCalls[], outcome, cost, tokens.
|
||||
- **GAP 1 — chat traces aren't addressable by the agent-traces route.** `GET /api/agents/:id/traces`
|
||||
(`agents.ts:469`) filters by the `agent:{id}` tag, but chat `start()` does NOT pass
|
||||
`tags:['agent:…']` (`chat.ts:1279-1285`). Fix = either add the `agent:` tag at chat
|
||||
`start()`, OR add a thin `GET /api/sessions/:id/traces` reading
|
||||
`traceStore.queryParsed({ sessionId })` (store already supports it, `execution-traces.ts:328`).
|
||||
- **GAP 2 — no frame↔trace backlink.** Nothing links a `memory_frame` to the
|
||||
`execution_trace` that produced it (no `trace_id`). So **per-frame "why is THIS memory here?"
|
||||
is MUST-BUILD** (add `trace_id` to frame metadata at write time + a `GET /api/memory/:id/trace`
|
||||
resolver). For PR3.5 the **honest, cheap win is a session/turn-scoped trace view** (the real
|
||||
reasoning + tool calls), NOT a per-frame reason. **Do NOT synthesize a "reason" string for a
|
||||
frame with no linked trace.** If neither route lands this arc, View B renders against a real
|
||||
recent session trace or is gated behind "no trace yet" empty state — never fabricated.
|
||||
|
||||
### Phase verification (each phase)
|
||||
Run the §1.4 gate after each phase; live-smoke the screen in dark + light with 0 console
|
||||
errors before moving on (matches PR3's shipping bar).
|
||||
|
||||
---
|
||||
|
||||
## 3. Honest data gaps (recon 05)
|
||||
|
||||
| Feature | REAL / DERIVABLE / MUST-BUILD | Plan |
|
||||
|---|---|---|
|
||||
| **source** (provenance class) | REAL (`memory_frames.source`) | Project per Part 1; FE owns the 5-value label map. |
|
||||
| **forget** (delete) | REAL (`DELETE /api/memory/:id`, hard delete + audit) | Wire button straight to it. No backend. |
|
||||
| **correct** (edit) | REAL (`PATCH /api/memory/:id`) | Wire edit/correct to it. No backend. |
|
||||
| **freshness / staleness** | DERIVABLE (`computeTemporalScore` over `created_at`, 7d boost/30d half-life) | FE compute or thin helper. **Present as honest age, never a stored "%".** |
|
||||
| **confidence** | PARTIAL (metadata blob; **harvest-only**, `undefined` for curated/agent/quick-capture frames) | **Show ONLY when present; hide the ring/badge when `undefined`. NEVER default a number** (PR3 streak-gate pattern). Stat #2 gated off if absent. |
|
||||
| **confirm / needs-confirm / verified** | MUST-BUILD (no per-frame confirm lifecycle; `unreviewed` never written) | Add cheap `POST /api/memory/:id/confirm` (set `metadata.status`), mirror `/archive`. Queue = `?status=unreviewed`. **Do NOT show "verified ✓" unless `source==='tool_verified'` or status explicitly set.** |
|
||||
| **per-frame "why?" trace** | MUST-BUILD backlink (no `trace_id` on frames); session/turn trace is DERIVABLE | Ship session/turn-scoped View B from real traces (add `agent:` tag or `/api/sessions/:id/traces`). **Never synthesize a reason for an unlinked frame.** |
|
||||
| **"Forgotten" filter** | NO DATA (hard delete = no tombstone) | **Gate the chip off / relabel** — there is no forgotten list. |
|
||||
|
||||
**Fabrication-risk flags (gate off, do not invent — PR3 streak precedent):**
|
||||
1. **Confidence** when undefined → hide ring, don't default.
|
||||
2. **Freshness** → age, never a stored decay %.
|
||||
3. **Per-frame trace reason** → only when a real linked trace exists.
|
||||
4. **"Forgotten" list** → no tombstone; gate off.
|
||||
5. **Stat-card numbers** → drive from the real query; a real 0 is honest, a fake `3`/`9`/`128` is not.
|
||||
|
||||
---
|
||||
|
||||
## 4. New primitives to build (recon 03 confirmed: 3 already exist — REUSE)
|
||||
|
||||
**Already exist in `components/ui/` (pre-PR3, commit `afe96355`) — do NOT rebuild:**
|
||||
`ConfidenceBadge`, `EvidenceChip`, `DetailDrawer`, `EvidencePanel`. The handoff's claim they'd
|
||||
be in `os/warm/` was a location error — they ship today via Memory Center.
|
||||
|
||||
**Genuinely new warm atoms for screen 19** (one-line spec each):
|
||||
|
||||
- **`ConfidenceRing`** — 38px circular ring; number + 2px border both colored by band (≥85
|
||||
`--healthy` / ≥60 `--attention` / <60 `--risk`); `CONF` mono caption beneath; **renders the
|
||||
"no value" neutral state when `confidence` is undefined** (no fabricated number).
|
||||
- **`ProvenanceLine` (extend, not new)** — add the §5b 3-segment form
|
||||
`⬡ <id> · source: <src> · ● <freshness>` (`source:`-prefix variant + `●` freshness segment
|
||||
colored healthy/attention); keep the existing `⬡ source · when` form + `onClick` → button.
|
||||
- **`TraceEvidenceChip`** (thin wrapper over `ui/EvidenceChip`) — multi-segment trace chip
|
||||
`confidence X% · source · when · still fresh` in `--surface`/`--line-soft`, `.src` refs mono
|
||||
`--intel`; only if the plain `EvidenceChip` can't render the multi-segment string directly.
|
||||
- **`DetailDrawer` (reuse)** — already exists; opened from a clickable memory row for the full
|
||||
memory detail (confidence/source/evidence/edit). No new build.
|
||||
|
||||
*(Net new code: `ConfidenceRing` + the `ProvenanceLine` extension; optional `TraceEvidenceChip`.)*
|
||||
|
||||
---
|
||||
|
||||
## 5. Risks & open questions for the founder
|
||||
|
||||
1. **SSE step pill — do it now (auto_recall slice) or defer?** Recon 02: in-scope only if
|
||||
paired with the `recallMemory()` return-shape widen (`orchestrator.ts:453-457`). If we want
|
||||
PR3.5 frontend-light, defer and keep PR3's affordance-only stance. **Recommend: do the
|
||||
`auto_recall` slice** (honest, load-bearing). Founder call on scope/budget.
|
||||
|
||||
2. **Token-family decision (recon 03 §1b):** warm `--intel` (provenance) vs Hive DS `--sem-*`
|
||||
(confidence bands) currently coexist. Screen 19's confidence rings use warm
|
||||
`--healthy/--attention/--risk`. Confirm: build `ConfidenceRing` against warm tokens and let
|
||||
`ui/ConfidenceBadge` keep `--sem-*`, or unify? Default: warm tokens for the ring (matches
|
||||
the mock 1:1).
|
||||
|
||||
3. **Confirm route + status writer.** `POST /api/memory/:id/confirm` is cheap, but nothing
|
||||
writes `unreviewed` today, so the "Awaiting your confirm" queue may legitimately be empty.
|
||||
Is that acceptable for launch (real-but-empty), or do we also wire the harvest-commit path
|
||||
to stamp `unreviewed` (larger)?
|
||||
|
||||
4. **Per-frame "why?" backlink.** True per-memory trace needs a `trace_id` on frame metadata
|
||||
at write time + a resolver (MUST-BUILD). For PR3.5, accept a **session/turn-scoped** View B
|
||||
(real traces, not per-frame)? Or invest in the backlink this arc?
|
||||
|
||||
5. **"Forgotten" filter has no data** (hard delete, no tombstone). Gate the chip off, relabel,
|
||||
or add a soft-delete tombstone (scope creep)? Recommend: gate off for now.
|
||||
|
||||
6. **Two-mind scope for screen 19.** `MemoryCenterApp` is two-mind (personal/workspace) and
|
||||
the Workspace Memory tab shares `MemoryCenterTab`. Does screen 19 replace `MemoryCenterApp`
|
||||
wholesale, or layer the Trust view as a new view alongside the existing 7
|
||||
(`memories·timeline·graph·harvest·weaver·wiki·evolution`)? Affects blast radius.
|
||||
62
docs/redesign-warm-hive/PR4-BUILD-PLAN.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Warm-Hive PR4 — Marketplace + shared install store ("sync") — Build Plan
|
||||
|
||||
> Source design: `docs/design_handoff_waggle_app/SCREENS.md §09`. Recon: `docs/redesign-warm-hive/pr4-recon/` (5 docs).
|
||||
> Branch: `feature/warm-hive-pr4` (off main @ 876807ca). Status: **plan — decisions §3 need founder ratification before feature code.**
|
||||
|
||||
---
|
||||
|
||||
## 1. The design contract (screen 09)
|
||||
|
||||
Skills + connectors + MCP as **one shelf**, agent-searchable. Two variations, **one store**:
|
||||
- **A — Browse & ask:** centered agent-search bar → suggestion box (connector + skill + tool, each with a "why" + install) · "N in this workspace" count bar · All/Skills/Connectors/MCP filter · card grid (kind badge, name, desc, install count, Add).
|
||||
- **B — Inline in chat:** the same picker mid-conversation — offer a missing connector with a **vault-aware approval** ("token → your vault") → connected follow-up.
|
||||
- **CRITICAL — "sync":** one store powers grid + agent-pick + inline. Type-aware one-click: **skill** Add→Adding…→Added (instant) · **connector** Connect→Signing in…→Connected (~1.1s, token→vault) · **MCP** Enable→Enabling…→Enabled. Each fires a toast + updates the count bar. **Installing in any view reflects in all.**
|
||||
|
||||
## 2. Current state (grounded in recon — the engine exists; the gap is surface + a store)
|
||||
|
||||
| Layer | Reality today | File anchors |
|
||||
|---|---|---|
|
||||
| **Match engine** | `searchCapabilities(need)` → ranked candidates across native tools / skills / marketplace, **each with `matchReason` ("why") + `installAction`**. Reachable ONLY as the `acquire_capability` chat tool, which returns markdown and **discards the structured proposal**. | `packages/agent/src/capability-acquisition.ts:179`; tool `skill-tools.ts:406` |
|
||||
| **Install backends (3, separate)** | skills = FS markdown reloaded into `agentState.skills`; connectors = **vault** credential `connector:<id>`; MCP = `.mcp.json` + live `McpRuntime` (PRO-gated, SecurityGate-scanned). **No unified installed-set.** | `routes/{skills,connectors,mcps}.ts`; `packages/marketplace/src/{db,installer}.ts` |
|
||||
| **Install verbs** | skill/pkg `POST /api/marketplace/install` (PRO) · MCP `POST /api/mcps/install` (PRO) · connector `POST /api/connectors/:id/connect` (token→vault, ungated). All write the append-only `install_audit` (no `workspace_id`). | `routes/marketplace.ts:181`, `mcps.ts:189`, `connectors.ts:96` |
|
||||
| **FE grid** | Phase-4B "Extend" surface: 6-facet rail, keyword search, **vertical list** of `ExtensionCard`, federates-at-read into **local `useState`** (discarded on unmount). Only marketplace skill/MCP pkgs install in-place; connectors/MCP = "Open in Hub" deep-links. Legacy tokens. | `MarketplaceApp.tsx` (418 LOC), `extend/ExtensionCard.tsx`, `lib/extension-catalog.ts` |
|
||||
| **Variation B precursor** | `CapabilityRequestCard` (inline chat) — **skill/marketplace only**, own private phase state, no connector/vault approval, no shared state with grid. Approval channel is **boolean-only** (can't carry a token). | `chat-blocks/CapabilityRequestCard.tsx`, `capability-request-parser.ts`, `confirmation.ts` |
|
||||
|
||||
**Net:** the matching brain + the 3 install paths are done and tested. PR4 is (a) a **shared FE install store** the three surfaces subscribe to, (b) **Variation A** UI (agent-search bar + suggestion box + count bar + grid with type-aware verbs/micro-states), (c) one **new route** exposing the existing engine, (d) **Variation B** completion (connector/MCP + vault approval).
|
||||
|
||||
## 3. Decisions — RATIFIED (founder, 2026-06-16): D1 FE-derived global count · D2 collapse to 4 shelves · D3 in-place (OAuth→Hub fallback) · D4 sync = grid+pick+inline. D5–D7 defaults stand.
|
||||
|
||||
| # | Decision | Recommended (v1) | Why / cost of the alternative |
|
||||
|---|---|---|---|
|
||||
| **D1** | "N in this workspace" count bar | **FE-derived global count** from existing per-type list endpoints | Real per-workspace tracking needs a new `workspace_id` migration across 3 backends + the OSS-excluded `install_audit` — large, and the substrate isn't workspace-partitioned. Honest label: "installed" not "in this workspace" if global. |
|
||||
| **D2** | Facets | **Collapse to the design's 4: All / Skills / Connectors / MCP** | Matches screen 09 ("one simple shelf"). Agents/Models/Templates keep their dedicated hubs (already deep-links). Keeping the 6-domain superset diverges from the design. |
|
||||
| **D3** | Connector/MCP in the grid | **In-place Connect/Enable** reusing the real vault/runtime + SecurityGate; **OAuth-only connectors fall back to the Connector Hub** | Design clearly wants in-place. Must NOT bypass security/OAuth gating. Token-paste connectors + MCP Enable work inline; OAuth-only can't finish inline. |
|
||||
| **D4** | Sync scope | **Grid + agent-pick + inline-chat** via an FE store; **re-hydrate on nav** so external Hub installs reconcile | Refactoring ConnectorsApp/MCPHubApp/skills-center to also push to the store is a much bigger blast radius — defer. |
|
||||
| D5 (default) | Suggestion "why" | engine's deterministic `matchReason` (free, honest, no model call) | LLM-generated why is prettier but adds a model call + latency. |
|
||||
| D6 (default) | Card install count | **omit the social `downloads` count** (local-single-user ≈ 0; honest-stats ethos) — show only a real Installed badge | — |
|
||||
| D7 (default) | PRO-gated items | show as installable with an **upsell on click** (current behavior; moat: skills/MCP are the upgrade trigger) | — |
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
- **Shared store (the spine):** new `apps/web/src/providers/InstallProvider.tsx` + `useInstallStore()` — an `installed` Map + `installing` Set keyed by `extension-catalog` namespaced ids; a **type-aware install dispatcher** (skill→`installPack`/skill; connector→`connectConnector` (vault); mcp→`installMcp`; pkg→`installMarketplacePackage`); a **count selector**; **optimistic flip + reconcile** (roll back on 403/SecurityGate, route to Upgrade/Approval). Hydrate from `GET /api/skills`, `/connectors`, `/mcps`, `/marketplace/installed` (no new read route for v1).
|
||||
- **One new server route:** `POST /api/marketplace/agent-search` `{need}` → wraps the existing `searchCapabilities()` (lift the dep assembly from `local/index.ts:620`), returns the **structured** proposal + a `pickOnePerKind()` grouping (connector+skill+tool). Mirrors `GET /api/skills/suggestions`. tsx-only → must `tsc -p packages/server`.
|
||||
- **Reuse:** `extension-catalog.ts` normalizers (flip `installable:true` for connector/mcp once the store can install them); `CapabilityRequestCard` for inline; warm components `components/os/warm/*` + warm tokens.
|
||||
|
||||
## 5. Phased plan (TDD; commit per phase; FE `tsc -p apps/web/tsconfig.app.json` + vitest each)
|
||||
|
||||
- **Phase A — shared install store** (`InstallProvider`/`useInstallStore`): hydrate, type-aware dispatcher, count selector, optimistic+reconcile, toasts. Unit-tested in isolation. *The spine everything else binds to.*
|
||||
- **Phase B — Variation A grid**: `ExtensionCard` type-aware verbs (Add/Connect/Enable) + idle→in-progress→done micro-states reading the store; `MarketplaceApp` sources from the store, adds the **count bar** + 4-shelf filter (D2) + **card grid** + warm tokens; in-place connector/MCP (D3). Rewrite the 11 phase4b tests to the new contract.
|
||||
- **Phase C — agent-search**: `POST /api/marketplace/agent-search` (+ `pickOnePerKind`) wrapping `searchCapabilities`; the centered **AskBar** + **suggestion box** (connector/skill/tool, each "why" + install via the store) + example chips.
|
||||
- **Phase D — Variation B inline**: extend `CapabilityRequest.kind` to `connector`/`mcp`; **vault-aware approval row** (token→vault; approval channel stays boolean, FE calls `connectConnector` directly — no token on the approval channel); route installs through the shared store so chat ↔ grid ↔ count bar stay in sync; consolidate `ApprovalGate`/`InlineApprovalCard`.
|
||||
- **Phase E — review + live smoke**: adversarial review workflow; live smoke (grid install reflects in count bar + inline; agent-search returns real picks; 0 console errors).
|
||||
|
||||
## 6. Key risks (from recon)
|
||||
- **11 phase4b tests** pin current testids + the `{type,limit:30}` call shape → Phase B rewrites them (expected, not collateral).
|
||||
- **Optimistic flips must reconcile** with server truth (install can 403 / SecurityGate-block *after* the flip) — across grid AND inline; count bar must not show a gate-rejected item.
|
||||
- **Don't bypass Hub security/OAuth** — in-place Connect/Enable reuse the real gated paths; OAuth-only → Hub fallback.
|
||||
- **`install_audit` is OSS-excluded + append-only** — never the source of truth for installed state; read per-type list endpoints.
|
||||
- **Honest-stats / mind-isolation ethos** — global count labeled honestly (D1); omit social download counts (D6).
|
||||
- Server routes run via `tsx` (not typechecked by `npm run build`) → explicit `tsc -p packages/server` gate on the new route.
|
||||
|
||||
## 7. Verification gates (per phase + final)
|
||||
`tsc -p apps/web/tsconfig.app.json` 0 · `tsc -p packages/server` 0 (new route) · FE vitest green (incl. rewritten phase4b) · `npm run lint` · live smoke: install in grid → count bar + inline both reflect; agent-search returns connector+skill+tool with "why"; vault-aware inline approval; 0 console errors.
|
||||
57
docs/redesign-warm-hive/PR5-BUILD-PLAN.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Warm-Hive PR5 — Settings (models-first + failover) · Onboarding (6-step + model gate) — Build Plan
|
||||
|
||||
> Source design: `docs/design_handoff_waggle_app/SCREENS.md §11` (Settings) + `§10` (Onboarding). Roadmap: `BUILD-PLAN.md §6` (PR5 = screens 11·10). Branch: `feature/warm-hive-pr5` (off main @ 8b4ba60a). Status: **plan — §3 decisions need founder ratification before feature code.**
|
||||
> Recon: 5-reader workflow `wf_11242c36-d4a` (5/5 areas), grounded vs live code.
|
||||
> **Status update (founder-ratified): D1 = BYO-key; D2–D12 proceed as recommended. Building.**
|
||||
|
||||
## 1. The design contract
|
||||
- **§11 Settings:** calm, progressive-disclosure; **Models leads.** Left rail General·Models·Permissions·Plan·Team·Backup·Advanced + a top-right **Show: Essential/Standard/Everything** (Advanced only at Everything). **Models:** a "≥1 working model" banner + the **Model-pilot failover chain** Primary→(if it errors)→Fallback→(daily budget)→Budget + a daily-budget input (switch at 80%) + read-only provider-key list (manage in Vault) + a local-models list. Maps to `defaultModel/fallbackModel/budgetModel + budgetThreshold/dailyBudget`.
|
||||
- **§10 Onboarding:** ≤2-min first-run ending **inside the work.** 6 full-screen steps: 1 Welcome · 2 About-you (name+role+team-size) · 3 **MODEL GATE** (API-key tab: provider chips + live-validated key→vault · OR local-model tab: detect Qwen / pull Llama — **hard gate: can't proceed without ≥1 working model**) · 4 Import · 5 Template · 6 First-task → opens into the workspace. The gate's **permanent home is Settings→Models** — same mechanism, shared.
|
||||
|
||||
## 2. Current state — **the engine + Settings are ~90% built; PR5 is reskin + reconcile + the onboarding gate**
|
||||
| Layer | Reality today | Anchors |
|
||||
|---|---|---|
|
||||
| **Failover engine** | **Fully live at runtime** — budget-threshold switch + smart cost routing + error-triggered fallback + budget-model context-compression, all per-request. NOT a UI concept. | `chat.ts:520-545,1112-1117,1374-1389`; `smart-router.ts:12-25` |
|
||||
| **Config + persistence** | All 5 fields (+`budgetHardCap`) have getters/setters + full GET/PUT `/api/settings`. | `core/config.ts:87-165`; `routes/settings.ts:66-129` |
|
||||
| **Settings shell** | 8-tab rail (General·Models·Billing·Permissions·Team·Backup·Enterprise·Advanced) + `ModelPilotCard` (3-lane Primary/Fallback/Budget + threshold slider) leads Models + read-only provider-key list. All 6 §11 "other tabs" built. | `SettingsApp.tsx:35-46,362-477,481-1037`; `ModelPilotCard.tsx` |
|
||||
| **Progressive disclosure** | EXISTS as the dock tier (Essential/Standard/Everything via `getSettingsTabsForTier`) — surfaced as a **"Dock Experience" `<select>` in the General body**, not a top-right rail control. Advanced shows at **Standard** (design wants Everything-only). | `lib/settings-tier-filter.ts:19-84`; `SettingsApp.tsx:244-260` |
|
||||
| **Provider catalog + key + local** | `GET /api/providers` (13 providers, `hasKey`, model catalogs, **live Ollama discovery**); `POST /api/settings/test-key` (**format-only**); a real **live Anthropic 1-token probe** exists but is private to `/health`; `/api/local-inference/{hardware,models,status,pull}`; vault key CRUD. | `routes/providers.ts:240-294`; `settings.ts:200-213`; `index.ts:2280-2331`; `local-inference.ts` |
|
||||
| **Onboarding** | **5-step** chain (first-launch·who-are-you·memory-import·workspace-create·ready), **NO model gate, NO template step**, first-task hardcoded. Nav is index-derived off `STEP_NAMES` (safe to re-key). Step-2 (name+role+team-size) + Import already built/rich. 15 templates exist in `constants.ts` but unwired. | `OnboardingWizard.tsx:35`; `onboarding/{WhoAreYouStep,ImportStep,constants}.tsx` |
|
||||
|
||||
**Net:** PR5 = (a) **one shared `ModelGate`** component (API-key validate→vault + local detect/pull + a `hasWorkingModel` signal), (b) **Settings reskin/reconcile** (warm tokens, Models-leads, "≥1 working model" banner, local-models list, label/disclosure tidy), (c) **Onboarding re-key 5→6 + the hard model gate + template + first-task steps + warm restyle**. The only backend bit is *optionally* generalizing the live key-probe (D3).
|
||||
|
||||
## 3. Decisions — **NEED FOUNDER RATIFICATION before feature code** (recommend-and-proceed unless you object, except D1)
|
||||
| # | Decision | Recommended | Why |
|
||||
|---|---|---|---|
|
||||
| **D1** | **BYO-key vs Waggle-metered** (DESIGN_POV §4 — the strategic gate; reshapes the gate copy + Billing/Usage PR7) | **Ship BYO-key** (paste a provider key→vault, or a local model) | 100% of the backend (vault, anthropic-proxy, local-inference) is BYO-shaped; metered = new billing/usage-cap plumbing. Matches the local-first promise + §10 copy. Metered = a later additive tab. **Founder strategic call.** |
|
||||
| **D2** | Hard-gate strictness + escape | **Hard** (Continue disabled until ≥1 working model) + **one** "I'll do this later" → dismiss onboarding to Home with a persistent "no model yet" banner | A zero-model first-task instantly errors (the cold-start churn DESIGN_POV §2 warns of); a zero-escape gate traps lookers. The dismiss-to-Home compromise keeps the gate's intent without a dead-end. |
|
||||
| **D3** | "✓ valid" validation depth | **Generalize the proven Anthropic 1-token live probe** to all providers behind `POST /api/settings/test-key` (live mode, 5s timeout + short-TTL hash cache); format-only fallback for providers w/o a cheap probe | Format-only would let a bad key pass the hard gate — defeating the "✓ valid" promise + the no-fabrication contract. The live pattern already exists (powers `/health`). **The one real backend addition in PR5.** |
|
||||
| **D4** | Onboarding step shape | **6 steps: Welcome · About-you · Model-gate · Import · Template · First-task**; fold workspace-creation into the Template step (template choice → `createWorkspace` w/ mapped persona → first-task opens it) | Matches §10 1:1 (no standalone workspace step); index-derived nav makes re-keying safe; reuses `handleCreateWorkspace`. |
|
||||
| **D5** | Templates on step 5 | The design's **curated 6** (Research/Strategy/Engineering/Sales/Writing/Custom) mapped to existing template ids + `TEMPLATE_PERSONA`; the other specialists via the workspace gallery later | §10 names 6 for the ≤2-min flow; 15 fights "calm/fast". Data + persona map already exist (curation, not new data). |
|
||||
| **D6** | Settings disclosure control | **Reuse** the existing `useOnboarding().tier` state; relocate/re-skin it as a top-right **Show:** segmented control. No second persistence key. | A second disclosure axis doubles state + contradicts the single global "how much to show" dial (already gates the dock). Shrinks the work to relocate+reskin. |
|
||||
| **D7** | Advanced visibility | Move `advanced` from STANDARD → **Power/Everything-only** in `settings-tier-filter.ts` | Matches the design's "depth when you ask for it"; keeps Standard calm. One-line behavior change to a shipped filter contract. |
|
||||
| **D8** | Rail labels | Rename **Billing → "Plan"**; **keep Enterprise** a separate tab (don't merge into Plan) | Rename = zero-risk, design-aligned. Merging Enterprise (KVARK config + audit gate) into a sales CTA risks regressions — defer. |
|
||||
| **D9** | Backup duplication | Settings Backup tab **reuses the richer standalone `BackupApp`** (embed or deep-link, PR4 Hub pattern) | `BackupApp.tsx` is strictly more capable (history/retry/metadata); two backup UIs = drift (CLAUDE.md §4). |
|
||||
| **D10** | Language selector (§11 General) | **DEFER** (note as out-of-scope) | No i18n/locale layer exists (0 grep hits); a real selector = a translation subsystem, unjustified now. Add the "local-first on-always" copy (trivial). |
|
||||
| **D11** | Warm-token sweep | **In scope** — kill hardcoded `hsl()` theme swatches + desaturate emerald/violet/amber/honey status colors to warm semantics | PR5 edits these files heavily; one pass keeps the "single honey accent" honest. (Teams-violet needs a remap call.) |
|
||||
| **D12** | Local-model pull UX | **Synchronous spinner** + success/fail toast (reuse `POST /api/local-inference/pull`); defer streamed progress | Streaming pull = net-new SSE backend off the critical path; most first-run users pick a cloud key. |
|
||||
|
||||
## 4. Architecture
|
||||
- **Shared `ModelGate` (the spine):** new `apps/web/src/components/os/model-gate/ModelGate.tsx` + `useHasWorkingModel()` — two tabs (API-key: provider chips + writable validated field → vault via `PUT /api/settings`; local-model: `getLocalInferenceStatus` detect + `pullLocalModel`), and `hasWorkingModel = activeProviders.length>0 || localStatus.totalLocalModels>0` (key "present"→"valid" once the live test passes). Mounted in **both** Onboarding step 3 AND as the Settings→Models lead (reskinning the existing `ModelPilotCard` + key-list into it). **Do NOT recreate** config getters, `chat.ts` failover, the providers route, or `ModelPilotCard`'s persistence contract.
|
||||
- **Backend (D3 only):** extend `POST /api/settings/test-key` with a live mode generalizing `validateAnthropicKey()` (1-token ping, !401/!403 = valid, 5s timeout, hash-keyed short TTL); `tsc -p packages/server`.
|
||||
|
||||
## 5. Phased plan (TDD; commit per phase; FE `tsc -p apps/web/tsconfig.app.json` + vitest each)
|
||||
- **Phase A — shared `ModelGate` + `useHasWorkingModel` + (D3) live key-validate.** The spine; unit-tested in isolation (mock providers/local/validate).
|
||||
- **Phase B — Settings reskin + reconcile.** Mount `ModelGate` as Models lead; "≥1 working model" banner; dedicated local-models list; warm-token sweep; Models-leads default tab; Billing→Plan (D8); Advanced→Everything-only (D7); top-right Show: control (D6); Backup reuses `BackupApp` (D9); General local-first copy (D10). Extend SettingsApp tests.
|
||||
- **Phase C — Onboarding 6-step + hard gate.** Re-key `STEP_NAMES` → 6 (D4); mount `ModelGate` as step 3 with the hard gate + "later"→Home+banner (D2); Template step (curated 6, wire persona + `createWorkspace` + `templateId`) (D5); First-task step (ask + suggested chips from `TEMPLATES[].hint` → seed first message → open workspace); warm full-screen restyle. Extend onboarding tests.
|
||||
- **Phase D — adversarial review + live smoke.** Review (correctness/security/honest-stats/design-fidelity); live smoke (onboarding blocks without a model + completes into the workspace with one; Settings Models reskin + banner; 0 console errors).
|
||||
|
||||
## 6. Key risks
|
||||
- **Don't recreate the failover engine / config / providers route / `ModelPilotCard` persistence** — reuse (CLAUDE.md §3.3/§8). The biggest risk is mistaking the ~90%-built Settings for net-new and rebuilding it.
|
||||
- **No-fabrication on "✓ valid"** — never show a confident valid on an unchecked/format-only key (carry the PR3/PR3.5 honesty contract).
|
||||
- **Onboarding re-key** is safe (index-derived nav) but shifts progress-dots/Back/Continue ranges + the `?forceWizard` latch + step-clamp — verify resume.
|
||||
- **Server route via tsx** (test-key live mode) not typechecked by `npm run build` → explicit `tsc -p packages/server`.
|
||||
- **D1 (BYO vs metered)** silently shapes the gate copy — ratify before the gate ships.
|
||||
|
||||
## 7. Verification gates (per phase + final)
|
||||
`tsc -p apps/web/tsconfig.app.json` 0 · `tsc -p packages/server` 0 (D3 route) · FE vitest green · `npm run lint` (no new errors) · live smoke: onboarding hard-gate blocks w/o a model + lands in the workspace with one; Settings Models reskin + working-model banner; 0 console errors.
|
||||
126
docs/redesign-warm-hive/PR6-BUILD-PLAN.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Warm-Hive PR6 — Remaining surfaces (the long tail) — Build Plan
|
||||
|
||||
> Source design: `docs/design_handoff_waggle_app/SCREENS.md §04·06·07·08·12·15·16·17·18` + `design-files/screens/*.html`. Roadmap: `BUILD-PLAN.md §6` (PR6 = screens 06·07·08·16·12·17·18·15·04). Branch: TBD off main (PR5 stream merges first). Status: **plan — §3 decisions need founder ratification before feature code.**
|
||||
> Recon: 9-screen grounded recon (one reader per screen), every claim verified vs live code with file:line. The notable corrections are in §8 (honesty log).
|
||||
> **Scope reframe vs the roadmap:** PR6 as written is **9 heterogeneous screens** — too big for one PR. §2.5 recommends a **3-way sub-PR split**.
|
||||
|
||||
## 1. The design contract
|
||||
- **§04 All workspaces** (`workspaces.html`): the full workspace shelf — Grid (default) / Table toggle, search + storage filter pills (All/Local/Virtual/Team), cards with hex avatar, **storage badge** (Local=honey, Virtual=intel, Team=healthy), summary, meta (memories · "agent live" · "N to review" · "Open →"). Answers "where are all my workspaces?" (Home = your day; this = everything). Reachable from rail/⌘K/switcher.
|
||||
- **§06 Launcher (AI-OS)** (`launcher.html`): detect & launch external coding agents into a workspace's shared memory. **Variation A (Launch)** = primary (tool cards, launch-with-task box, "Running now" PID panel). **Variation B (How memory is shared)** = a 3-node flow diagram + 3 explanation cards + a provenance example, on a segmented toggle.
|
||||
- **§07 Storage & Files** (`storage.html`): make the filesystem legible. **Variation A (Where it lives)** = live on-disk tree card + three storage-type cards (Virtual/Local/Team) + local-first reassurance. **Variation B (Files)** = classic browser with a **Source/provenance column** ("made by Claude Code"). Both real, one screen, segmented toggle. Maps to `StorageType = 'virtual'|'local'|'team'`.
|
||||
- **§08 Power surfaces** (`surfaces.html`): the long tail (Tools · Automations · Approvals · Vault · Usage) on **ONE reusable row/card/toggle set**, behind ⌘K. Per-model spend bars + budget on Usage; Rotate/Reveal/Renew on Vault; color-stratified risk badges on Approvals; enable/pause on Automations.
|
||||
- **§12 Skill evolution + diffusion** (`evolution.html`): **A (version ladder)** = a skill card with v1/v2/v3 quality scores + improvement notes + a "GEPA optimized · judged by 3 models" provenance badge. **B (diffusion)** = skills/recipes propagating across agents/workspaces via waggle-dance signals. Both views are real tabs per §371.
|
||||
- **§15 Habit loop** (`habit.html`): **NOT a standalone screen** — a principles doc whose mechanics live on Home: 🔥 daily streak chip, "while you slept" variable-reward chips, gentle nudges (earned), compounding memory depth. Ethics note (no dark patterns, all toggle-off-able).
|
||||
- **§16 App surfaces (sextet)** (`appsurfaces.html`): six real routes on one rail — Agents (swarm CTA), **Room** (shared stage of turns + participants panel), **Artifacts** (3-col card grid + provenance), **Mission Control** (hive health + local services list), Timeline (one chronological stream), **Profile** ("what Waggle knows about you" facts + persona chips).
|
||||
- **§17 Benchmarks** (`benchmark.html`): honest positioning. **A (Capabilities)** = 11-row matrix (Waggle column honey-highlighted; ●/◐/○; competitors deliberately given ● on terminal coding). **B (Memory SOTA)** = LoCoMo bars (87.66/81.95/78.05/62.47) + stats (+5.71pp, p<10⁻⁵, 92.75%, 100% local). ⌘K-only, both views real tabs. Numbers must stay accurate to the `hive-mind` benchmark.
|
||||
- **§18 Platform & roadmap** (`platform.html`): the Tauri desktop story + roadmap. **4 views** (all real tabs per §371): Desktop (macOS↔Windows titlebar toggle), Boot ("warming the hive" checklist), Coming-next channel cards, and a **custom 404** ("This cell of the hive is empty."). ⌘K-only.
|
||||
|
||||
## 2. Current state — **grounded, one row per screen** (verified vs live, 2026-06-17)
|
||||
| Screen | Design intent (1-line) | Reality today (file:line) | State | Gap | Effort |
|
||||
|---|---|---|---|---|---|
|
||||
| **04 All workspaces** | Full shelf: grid/table + storage pills + rich cards | `App.tsx:66` `/workspaces`→`Navigate to /home`; list lives only as `HomeCockpit` RecentWorkspacesPanel (`HomeCockpit.tsx:166-226`, ~2 cards). `adapter.getWorkspaces()` (`adapter.ts:491-500`), `Workspace` type w/ `storageType/memoryCount/sessionCount/health` (`types.ts:239-260`), `useWorkspaces` hook all present. No `AllWorkspacesApp`, no ⌘K/rail entry. | **NET-NEW** | large | **M** |
|
||||
| **06 Launcher** | Variation A launch + Variation B memory-sharing explainer | `LauncherApp.tsx:1-525` feature-complete A (detect/launch/hooks/processes/prompt-pass). Routed `LauncherRoute`, in `App.tsx:82`, `command-catalog.ts:72`, dock `dock-tiers.ts:93`. Backend `/api/tools/{detect,launch,processes,kill,hooks}` (`adapter.ts:2408-2523`). **Variation B absent.** | **EXISTS (PARTIAL)** | small | **S** |
|
||||
| **07 Storage & Files** | A "where it lives" + B file browser w/ provenance col | `FilesApp.tsx:1-793` (B browser; **no Source column**, headers Name/Size/Modified `:539-544`), `FilesAppTabs.tsx:1-99` (storage-type tabs — orthogonal to A/B), `FilesRoute.tsx`, `STORAGE_LABELS` (`file-utils.ts:52-56`). **Variation A absent** (no on-disk tree, no storage-type cards). | **PARTIAL** | medium | **M** |
|
||||
| **08 Power surfaces** | 5 tabs on ONE row/card/toggle set, ⌘K shell | 5 separate apps at separate routes: `VaultApp.tsx` (`/settings/vault`; Reveal+Rotate-link, **no Renew**), `ApprovalsApp.tsx` (`/approvals`; amber text, **no color-stratified badges**), `AutomationCenterApp.tsx` (`/automations`; toggles work), `TelemetryApp.tsx:1-145` (`/settings/usage`; **no per-model bars, no budget**), `CapabilitiesApp.tsx` Tools tab ≠ integrations. Backend all present (vault/approval/automations/cost/events). | **PARTIAL** | medium | **M** |
|
||||
| **12 Evolution** | A version ladder + B diffusion maps | `EvolutionTab.tsx:1-1000` full run-review (list/detail/accept/reject/SSE/NewRunModal), rendered as Memory tab (`MemoryCenterApp.tsx:35,180`). Backend `evolution.ts:1-662` complete. **No version-ladder skill-card framing; no diffusion (B) UI.** | **PARTIAL** | medium | **M** |
|
||||
| **15 Habit loop** | Mechanics on Home (streak/overnight/nudge/depth) | `StreakChip.tsx:1-26` exists but `SHOW_STREAK=false`, `STREAK_DAYS=0` hardcoded (`HomeCockpit.tsx:49-50`). `OvernightHero.tsx`+`RunChip.tsx` correct & live (`HomeCockpit.tsx:293-330`). `NotificationInbox.tsx:1-80` exists but **not wired to Home**. No `streak/memoryCount/nudge` on `HomeBriefing` (`types.ts:317-329`). Memory-depth: **no backend**. | **PARTIAL** | medium | **M** (backend-blocked) |
|
||||
| **16 App surfaces (sextet)** | 6 routes, consistent rows/badges/provenance | All six routed: `AgentsApp` (swarm CTA soft-routed `:158`), `RoomApp.tsx:1-261` (**single-col tile grid; no participants panel/turn stage**), `ArtifactCenterApp.tsx:1-386` (**vertical list; not 3-col grid**), `MissionControlApp.tsx:1-227` (fleet/team/activity tabs; **no unified services list**), `TimelineApp.tsx:1-241` (**ALIGNED**), `UserProfileApp.tsx:1-532` (**no "what Waggle knows" read-only facts**). | **PARTIAL** | medium | **M** |
|
||||
| **17 Benchmarks** | Capabilities matrix + Memory SOTA bars, ⌘K-only | **Nothing exists.** No grep hits for "benchmark" in `apps/web/src`; not in `dock-tiers.ts` AppId, `routes.ts`, `command-catalog.ts`, `App.tsx`. Design fully specified in `benchmark.html` (220 lines, static data). No backend needed. | **NET-NEW** | net-new | **M** |
|
||||
| **18 Platform & roadmap** | 4 views (Desktop/Boot/Roadmap/404), ⌘K-only | **Nothing exists.** No `PlatformApp`/`PlatformRoute`, not in AppId/routes/⌘K/`App.tsx`. 404 is generic `NotFound.tsx:1-24` (no honey hex, no spec copy). Design in `platform.html`. No backend needed. | **NET-NEW** | net-new | **M** |
|
||||
|
||||
**Net:** PR6 is **2 net-new informational surfaces** (17, 18 — pure UI, static data, no backend), **1 net-new data surface** (04 — backend already feeds it), **5 reskin/reconcile-in-place** of shipped apps (06 add Variation B; 07 add Variation A + provenance col; 08 add the 5 missing features within existing routes; 12 add version-ladder framing; 16 reskin 5 of 6 + 2 small builds), and **1 backend-blocked Home wiring** (15 — flip the streak gate + the depth/nudge mechanics need new backend fields, so the flip-now slice is small and the rest defers).
|
||||
|
||||
### 2.5 Recommended sub-PR split (the core recommendation)
|
||||
PR6 spans nine screens with sharply different shapes — net-new builds, in-place reskins, and a backend-blocked feature. One PR would be unreviewable and would couple low-risk reskins to backend-dependent work. **Recommend 3 cohesive sub-PRs, ordered by value/risk:**
|
||||
|
||||
| Sub-PR | Screens | Scope theme | Why this grouping | Effort |
|
||||
|---|---|---|---|---|
|
||||
| **PR6a — Net-new informational surfaces** | **17 Benchmarks · 18 Platform** | Two pure-UI, static-data, ⌘K-only surfaces. New `*App` + `*Route` + register in `routes/index.ts`+`App.tsx`+`command-catalog.ts`; reskin `NotFound.tsx`. | **Lowest risk, highest standalone value** — zero backend, no existing-component blast radius, ships the public-facing positioning + desktop story. Benchmarks shows the SOTA proof we already published. Cleanest first PR. | **S–M** |
|
||||
| **PR6b — In-place reskin + feature-fill of shipped apps** | **06 Launcher · 07 Storage&Files · 08 Power surfaces · 12 Evolution · 16 App-surfaces** | Reskin shipped apps to spec + fill named missing features, each at its current route. No architectural unification, no new backend (except §3 D3/D6 cheap routes). | **The bulk of the value, all reuse.** Every screen already has a working component + backend; this is "make it match the doc." Largest review surface, so it is its own PR. Internally phaseable (one screen per commit). | **L** (M each ×5) |
|
||||
| **PR6c — All workspaces + Habit-on-Home** | **04 All workspaces · 15 Habit loop** | The two surfaces that need a backend field/route to be honest: 04 = net-new view over existing `getWorkspaces`; 15 = flip the streak gate + (deferred) depth/nudge fields. | **Backend-coupled, so isolated from the reskins.** 04 is a real new screen but its data is ready; 15's honest slice is tiny (gate flip) with the rest gated behind new `HomeBriefing` fields. Grouping the two data-shaped surfaces keeps the backend touch in one PR. | **M** |
|
||||
|
||||
**Sequencing rationale:** PR6a first (de-risked, shippable alone, no dependencies). PR6b second (the meat; depends on PR1/PR2 tokens + ⌘K being merged, which they are). PR6c last (carries the only net-new backend fields — §3 D9/D10 — and is the most likely to spill into a follow-up). Each sub-PR is independently revertible. If timeline pressure hits, **PR6c is the natural defer candidate** (04 can ship without a rail entry; 15's flip can wait for the streak backend).
|
||||
|
||||
## 3. Decisions — **NEED FOUNDER RATIFICATION before feature code** (recommend-and-proceed unless you object; D1/D8/D11 are strategic)
|
||||
| # | Decision | Recommended | Why |
|
||||
|---|---|---|---|
|
||||
| **D1** | **Sub-PR split** (§2.5) | **Ship the 3-way split: PR6a (17·18) → PR6b (06·07·08·12·16) → PR6c (04·15)** | Nine heterogeneous screens are unreviewable as one PR and couple zero-risk reskins to backend-dependent work. The split groups by risk shape, keeps net-new backend in one PR, and each piece is independently revertible. **Founder structural call.** |
|
||||
| **D2** | Benchmarks (17) — build vs defer | **BUILD in PR6a** | Pure UI, static data from `benchmark.html`, no backend, self-contained. It surfaces the LoCoMo SOTA proof (87.66, +5.71pp, p<10⁻⁵) that is **already published** — high marketing value at near-zero risk. ⌘K-only keeps the spine calm. |
|
||||
| **D3** | Benchmarks data source | **Inline constant in `BenchmarkApp.tsx`** (11×6 CAPS + 4 SOTA bars), mirroring `benchmark.html` | Design treats it as static. An API is over-engineering for numbers that change on a benchmark cadence, not a request cadence. Numbers must be **manually refreshed from the `hive-mind` benchmark** (carry a dated comment + the no-fabrication caveat). |
|
||||
| **D4** | Platform (18) — build vs defer | **BUILD in PR6a** | Pure UI, no backend, self-contained, ⌘K-only. Tells the Tauri desktop story + roadmap and replaces the generic 404 with the on-brand one. Low risk, high polish payoff. |
|
||||
| **D5** | Platform 404 ownership | **Reskin the standalone `NotFound.tsx`** (honey hex + spec copy + back-to-Home + ⌘K affordance); **do NOT** nest 404 inside `PlatformApp` state | `NotFound.tsx` is the real router catch-all (`App.tsx:94`); a tab inside `/platform` would never catch a bad URL. Keep them split: 404 stays the route fallback, Platform's other 3 views are tabs. |
|
||||
| **D6** | Platform Boot view vs real boot | **Render Boot as a static showcase tab** in `PlatformApp`; do NOT rewire the live `BootScreen`/AppShell boot gate | The real boot flow is owned by AppShell (PR1 stream). The §18 "warming the hive" view is a marketing showcase, not the runtime gate — duplicating runtime logic risks the cold-load crash class PR3 already fought. Showcase only. |
|
||||
| **D7** | Launcher (06) Variation B | **Add as a segmented toggle on the same surface** (matches `launcher.html:110-113`); align copy to §06 spec | Design shows a segmented control on one page, not a modal/overlay. All plumbing exists; this is a second view + copy polish. No backend. |
|
||||
| **D8** | Power surfaces (08) — unify behind ⌘K vs keep separate routes | **Keep the 5 apps at their current routes; reskin to a shared row/card/toggle look. DO NOT unify into one `/settings/power-surfaces` shell in PR6.** | Unifying breaks 4 deep-linked routes (`/settings/vault`, `/approvals`, `/automations`, `/settings/usage`) wired into ⌘K + dock + onboarding. The design's "ONE component set" intent is satisfied by **shared primitives**, not a shared route. Architectural unification is a **post-launch Phase 4B item** — flag it but don't ship it here. **Strategic scope call.** |
|
||||
| **D9** | Power surfaces (08) feature fill | **In PR6b:** per-model spend bars + daily-budget input (80% warn) on Usage; Renew button on Vault → reuse the Rotate endpoint; color-stratified risk badges (amber=Medium, green=Low) on Approvals; harmonize toggle/row/badge styling | These are the named §08 gaps and are all FE-side over existing backends (`cost/by-workspace`, vault, approval). Renew has no dedicated endpoint — **point it at Rotate** and note the intent for a future phase (no new backend in PR6). |
|
||||
| **D10** | Storage (07) — A as a tab in `/files` vs new `/storage` route | **Tab within `/files`** via a new `StorageAndFilesApp` wrapper holding the A/B segmented toggle; storage-type tabs (Virtual/Local/Team) live **inside** Variation B (orthogonal) | Design shows one screen with a unified A/B switcher (`storage.html:97-100`). Reuses `FilesRoute`; avoids a second route + a second ⌘K entry. The recon's key correction: the existing storage-type tabs are a **different axis** from A/B — they coexist, not conflate. |
|
||||
| **D11** | Storage (07) provenance column — source | **Add a Source column to the file table** showing granular creator ("made by Claude Code", "you uploaded", agent name); **gate off when unknown** (no fabricated source) | §07 shows the specific string ("made by Claude Code"). Needs the creator/provenance to exist on the file metadata — **verify the file-tree route carries it; if absent, show "—", never invent** (PR3/PR3.5 no-fabrication contract). Confirm data availability before shipping the column. |
|
||||
| **D12** | Evolution (12) version-ladder grouping | **In PR6b:** aggregate `EvolutionRun` rows by `(target_kind, target_name)` into skill cards; render the version ladder (score bar + delta + improve-note); **DEFER Variation B (diffusion maps) to PR7/8** | A is a reskin over real run data (`evolution.ts`). B (diffusion) is **net-new UI** (propagation maps + adopter lists + waggle-dance signal correlation) — out of proportion to a reskin. Signals exist in backend but the map component is a distinct feature. |
|
||||
| **D13** | Evolution (12) "GEPA · judged by 3 models" badge | **Show only when derivable from `gate_reasons`/status; otherwise omit** | The backend does not explicitly store this provenance string. Inferring "best" from `(status=deployed, highest delta_accuracy)` is honest; **fabricating "judged by 3 models" when the gate roster isn't recorded is not.** Gate the badge off when the metadata is absent. |
|
||||
| **D14** | All workspaces (04) — Grid vs Table MVP | **Ship Grid (default) only in PR6c; Table as a fast-follow** | Design names Grid as the ship variation (§366); Table is the alternate. Grid-only is the ≤2-screen MVP; the toggle scaffold can land with Table stubbed/deferred. |
|
||||
| **D15** | All workspaces (04) — nav entry | **Add an "All workspaces" entry to ⌘K (Jump group, after Home) in PR6c**; replace the `App.tsx:66` `/workspaces`→`/home` redirect with the real route | §04 says it's reachable from rail/⌘K/switcher. ⌘K is the cheapest discoverable entry (no spine change — Home stays the day view). Rail entry is out of scope (5-item spine is locked). |
|
||||
| **D16** | All workspaces (04) — empty state | **Surface a first-run empty state** ("Create your first workspace" CTA) rather than gating behind Home | A direct `/workspaces` visit (or ⌘K) on a zero-workspace install must not dead-end. Reuse the Home create-workspace flow's dialog. |
|
||||
| **D17** | Habit (15) — streak gate flip | **In PR6c:** flip `SHOW_STREAK=true` **only after** a real `streak` field lands on `HomeBriefing` (backend); until then leave gated (no fabricated streak) | A hardcoded `STREAK_DAYS=0` or invented count violates the no-fabrication contract (PR3 streak precedent — this is literally that precedent). The flip is one line; the **backend streak calculation is the dependency** — confirm owner + timing. |
|
||||
| **D18** | Habit (15) — memory-depth + nudges | **DEFER memory-depth ("+14 this week · top 8% by depth") to post-PR6** (needs a new `/api/memory/stats` shape + `HomeBriefing` fields); **wire gentle nudges only if `NotificationInbox` filtering lands**, else defer | Depth requires net-new backend (no `memoryCount`/`depthRank` today). Nudges must be "earned" per §15 ethics — surfacing all notification types uniformly is the dark-pattern the design warns against. Both are backend-shaped; keep PR6c's habit slice to the honest streak flip + the already-correct OvernightHero. |
|
||||
| **D19** | App-surfaces (16) builds vs reskins | **In PR6b:** reskin all six to warm tokens; **build** the Room participants-panel/turn-stage (two-col) + Artifacts 3-col grid + Profile "what Waggle knows" read-only facts; **promote** the Agents swarm CTA; Mission Control = relabel tabs (don't rebuild as a flat services list) | These are the named §16 gaps. Room/Artifacts/Profile are real layout builds; Agents/Mission-Control are reskins/relabels. Mission Control's flat-services-list redesign is larger than a reskin — **relabel + reorder tabs to lead with Services**, defer the full rebuild. |
|
||||
| **D20** | Profile (16) "what Waggle knows" facts source | **Derive from stored `profile.identitySuggestions` first; only add `/api/profile/facts` if that's insufficient** | Avoids a net-new route if the data already exists in the profile blob. Confirm the suggestions field carries displayable facts before committing to a new endpoint. **No fabricated facts** — render only what the profile actually holds. |
|
||||
| **D21** | Warm-token sweep | **In every sub-PR, sweep the touched files** — desaturate emerald/violet/amber/sky status colors (still hardcoded in `dock-tiers.ts` color fields + the older apps) to warm semantics; verify `--honey/--intel/--healthy/--attention/--risk` usage matches the mocks | PR6 edits these surfaces heavily; one pass per file keeps the "single honey accent" honest. The sextet + power surfaces still carry pre-warm tier colors (`text-violet-400`, `text-emerald-400`, etc.). |
|
||||
|
||||
## 4. Architecture
|
||||
- **PR6a (net-new, no backend):**
|
||||
- `apps/web/src/components/os/apps/BenchmarkApp.tsx` (state-toggled Capabilities/SOTA views; inline `CAPS` 11×6 + `SOTA_BARS` 4-bar constants, dated, with the no-fabrication caveat verbatim) + `routes/BenchmarkRoute.tsx` (thin wrapper, PR1a §5.1 pattern). Register in `routes/index.ts` + `App.tsx` (`/benchmarks`) + `command-catalog.ts` (Power group, ⌘K-only).
|
||||
- `apps/web/src/components/os/apps/PlatformApp.tsx` (3 tabs Desktop/Boot/Roadmap via a `view` enum; Desktop has a macOS↔Windows titlebar toggle) + `routes/PlatformRoute.tsx` + register `/platform` (⌘K-only). Reskin `pages/NotFound.tsx` (honey hex + "This cell of the hive is empty." + back-to-Home + ⌘K).
|
||||
- **PR6b (reskin-in-place, reuse — DO NOT recreate the backing apps/routes/backends):**
|
||||
- **06:** extend `LauncherApp.tsx` with a Variation B view behind a segmented toggle (flow diagram + 3 cards + provenance example). No route/backend change.
|
||||
- **07:** new `StorageApp.tsx` (Variation A: live on-disk tree card + 3 storage-type cards, fed by `GET /api/workspaces/:id` + the file-tree route) + new `StorageAndFilesApp.tsx` wrapper (A/B segmented toggle, hosts `FilesApp` for B) routed via `FilesRoute`; add a Source column to `FilesApp` (`:539-544`, gated when provenance absent).
|
||||
- **08:** add per-model bars + budget UI to `TelemetryApp`; Renew→Rotate in `VaultApp`; color-stratified risk badges in `ApprovalsApp`; extract shared `<SurfaceRow>/<SurfaceToggle>/<RiskBadge>` warm primitives (the "ONE component set") and adopt across the 5 apps. **No route consolidation (D8).**
|
||||
- **12:** in `EvolutionTab.tsx`, add a skill-card aggregation + version-ladder render over existing `EvolutionRun` data; provenance badge gated (D13). No backend.
|
||||
- **16:** reskin six apps; build Room two-col (participants panel + turn stage), Artifacts 3-col grid, Profile read-only facts section; promote Agents swarm CTA; relabel Mission Control tabs.
|
||||
- **PR6c (data surfaces, the only net-new backend):**
|
||||
- **04:** new `AllWorkspacesApp.tsx` (grid + search + storage filter pills + cards w/ actions via `useWorkspaces`/`adapter.getWorkspaces`/`ShellContext.selectWorkspace`) + `routes/WorkspacesRoute.tsx`; replace `App.tsx:66` redirect; add ⌘K entry. Reuse `WorkspaceActionsMenu` for row actions. No new backend (all routes exist: `GET /api/workspaces`, `DELETE/PATCH /api/workspaces/:id`).
|
||||
- **15:** flip `SHOW_STREAK` once a real `streak` lands on `HomeBriefing` (the **one backend field** — calculation owner TBD, D17); OvernightHero is already correct; depth/nudges deferred (D18).
|
||||
|
||||
## 5. Phased plan (TDD; commit per phase/screen; FE `tsc -p apps/web/tsconfig.app.json` + vitest each)
|
||||
**PR6a**
|
||||
- **A1 — Benchmarks.** `BenchmarkApp` + route + ⌘K register; inline data constants; dark+light; matrix + SOTA-bar render; unit test (renders both views, honey-highlight on Waggle column/bar).
|
||||
- **A2 — Platform + 404.** `PlatformApp` 3 tabs + route + ⌘K register; reskin `NotFound`; unit test (tab switch, titlebar toggle, 404 affordances).
|
||||
|
||||
**PR6b** (one commit per screen; each: reskin + named feature + tests)
|
||||
- **B1 — Launcher Variation B** (segmented toggle + flow/cards/provenance + copy).
|
||||
- **B2 — Storage Variation A + provenance column** (`StorageApp` + `StorageAndFilesApp` wrapper + `FilesApp` Source column gated; confirm D11 data first).
|
||||
- **B3 — Power surfaces feature-fill** (per-model bars + budget; Renew; risk badges; shared primitives — D8/D9/D21).
|
||||
- **B4 — Evolution version ladder** (aggregate + ladder + gated badge — D12/D13).
|
||||
- **B5 — App-surfaces sextet** (reskin 6 + Room/Artifacts/Profile builds + Agents CTA + Mission Control relabel — D19/D20).
|
||||
|
||||
**PR6c**
|
||||
- **C1 — All workspaces** (`AllWorkspacesApp` + route replacing redirect + ⌘K entry + grid + actions + empty state — D14/D15/D16).
|
||||
- **C2 — Habit streak flip** (flip `SHOW_STREAK` gated on the real `streak` field; verify OvernightHero; depth/nudges deferred — D17/D18).
|
||||
|
||||
**Each sub-PR ends with:** adversarial review (correctness / security / honest-stats / design-fidelity) + live smoke (dark + light, 0 console errors).
|
||||
|
||||
## 6. Key risks
|
||||
- **Don't recreate the 5 backing apps / routes / backends for screens 06·07·08·12·16** — they are shipped and working; PR6b is reskin + feature-fill, not rebuild (CLAUDE.md §3.3/§8). The biggest risk is mistaking a PARTIAL app for net-new.
|
||||
- **No-fabrication contract (the recurring trap):** Benchmark numbers (D3), Evolution "judged by 3 models" badge (D13), Storage provenance column (D11), Profile facts (D20), Habit streak/depth (D17/D18) are **six** places PR6 could invent data. Gate each off when the real value is absent — carry the PR3/PR3.5 streak-gate precedent. A real 0 is honest; a fabricated count is not.
|
||||
- **Power-surfaces route consolidation (D8)** is a deep-link landmine — 4 routes are wired into ⌘K + dock + onboarding. Reskin to shared primitives, do **not** collapse the routes in PR6.
|
||||
- **All-workspaces redirect replacement (`App.tsx:66`)** changes router behavior — verify nothing else relies on `/workspaces`→`/home` (it does NOT per recon; switcher opens a picker modal). Add the empty-state to avoid a zero-workspace dead-end.
|
||||
- **Habit streak is backend-blocked (D17)** — the flip is one line but the streak calculation owner is unconfirmed. Don't flip until the field is real, or the gate ships a fabricated streak.
|
||||
- **404 ownership (D5)** — keep `NotFound.tsx` the router fallback; a Platform tab can't catch bad URLs. Don't merge them.
|
||||
- **Server routes via tsx** (D3-class cheap routes, if any land for Profile facts/file-tree provenance) are not typechecked by `npm run build` → explicit `tsc -p packages/server`.
|
||||
|
||||
## 7. Verification gates (per sub-PR + final)
|
||||
`tsc -p apps/web/tsconfig.app.json` 0 · `tsc -p packages/server` 0 (only if any cheap route lands) · FE vitest green · `npm run lint` (no new errors) · live smoke per sub-PR (dark + light, 0 console errors):
|
||||
- **PR6a:** Benchmarks renders both views w/ honey-highlight + accurate numbers; Platform 3 tabs + titlebar toggle; custom 404 on a bad URL; both reachable via ⌘K.
|
||||
- **PR6b:** Launcher A/B toggle; Storage A/B toggle + provenance col (or honest "—"); Usage per-model bars + budget; Vault Renew; Approvals color badges; Evolution version ladder; sextet reskinned + Room two-col + Artifacts grid + Profile facts.
|
||||
- **PR6c:** `/workspaces` renders the shelf (grid + search + pills + actions + empty state) reachable via ⌘K; streak chip shows only with a real field.
|
||||
|
||||
## 8. Honesty log — every drift the recon found (verified vs live)
|
||||
- **04:** `BUILD-PLAN.md §9.7` makes **Home the workspace selector** (Home `RecentWorkspacesPanel` shows ~2 cards, `HomeCockpit.tsx:166-226`), so §04's "reachable from rail" is **not literally true** — no rail entry exists (5-item spine locked) and the switcher pill opens a **picker modal, not the full shelf**. PR6c adds a ⌘K entry, not a rail entry. Design shows 8 sample workspaces; live has no dedicated view at all.
|
||||
- **06:** `launcher.html` has BOTH variations (segmented control `:110-113`); the React `LauncherApp` renders **only Variation A** — no toggle, no Variation B. Copy is generic ("Tool Launcher", "Optional prompt") vs the spec narrative.
|
||||
- **07:** The existing `FilesAppTabs` storage-type tabs (Virtual/Local/Team) are **orthogonal to** the A/B variation toggle — the recon's key correction is they must **coexist, not conflate** (storage-type filters live *inside* Variation B). The file table has **no Source/provenance column** (`:539-544` = Name/Size/Modified only). Variation A has no component, route, or on-disk tree at all.
|
||||
- **08:** Design assumes a **unified ⌘K shell with 5 tabs via a left-rail switcher**; live has **5 separate full-canvas apps at separate routes** — an architectural split, not a styling gap. `VaultApp`'s Connectors tab was **moved out** to a separate `ConnectorsApp` (`:229` comment). `CapabilitiesApp.Tools` shows **generic agent tools** (read_file/bash/web_search), **not connected integrations** (those live in `ConnectorsApp`) — the design's "Tools" tab and the live "Tools" tab are different things. No Renew, no per-model bars, no budget UI, no color-stratified risk badges. Each app uses **distinct styling** (the "ONE component set" intent is unmet).
|
||||
- **12:** Implementation tracks **individual runs**, design shows a **single skill card with 3 versions** — needs `(target_kind, target_name)` aggregation. Backend does **not** store "GEPA optimized · judged by 3 models" provenance (must infer or omit, D13). Variation B (diffusion) has **zero FE integration** despite waggle-dance carrying `skill_share`/`model_recipe` subtypes — it's a net-new feature, not a reskin.
|
||||
- **15:** `SHOW_STREAK=false` + `STREAK_DAYS=0` are **intentional gates** awaiting a backend `streak` field (review must read them as TODO, not bugs). `OvernightHero`+`RunChip` are **already correct and live** (no work). `NotificationInbox` exists but is **isolated** (not wired to Home; renders ALL notification types uniformly — the opposite of the "earned nudge" the design demands). Memory-depth has **zero backend** (`HomeBriefing` has no `memoryCount`/`depthRank`). Correctly has no standalone `/habit` route (matches spec).
|
||||
- **16:** Room = **single-column tile grid**, design wants **two-col** (shared turn stage + participants panel). Artifacts = **vertical list**, design wants **3-col card grid** with ext tiles + provenance. Profile = editable tabs only, **missing the "what Waggle knows" read-only facts** section. Mission Control = fleet/team/activity tabs, design leads with a **local-services list**. Agents swarm CTA exists but is **soft-routed/buried** (`:158`). Timeline is **fully aligned** (no work).
|
||||
- **17:** **Net-new** — verified zero "benchmark" grep hits in `apps/web/src`; absent from AppId/routes/⌘K/`App.tsx`. Matrix data is hardcoded JS in `benchmark.html:168-180` (must inline or externalize). Numbers are June-2026-dated and must be **manually refreshed** from the `hive-mind` benchmark — they are accurate today (87.66 SOTA matches the published result + MEMORY.md SOTA index).
|
||||
- **18:** **Net-new** — no `PlatformApp`/`PlatformRoute`; absent from AppId/routes/⌘K/`App.tsx`. The live 404 is the **generic `NotFound.tsx:1-24`** (no honey hex, no spec copy, no ⌘K affordance). The §18 Boot "warming the hive" view risks **colliding with the real AppShell boot gate** — keep it a static showcase (D6), don't rewire runtime.
|
||||
- **Cross-cutting:** the BUILD-PLAN §6 PR6 row lists all 9 screens as one PR; this plan reframes that into the §2.5 split. Dock-tier `color` fields and the older apps still carry pre-warm tier colors (`text-violet-400`, `text-emerald-400`, `text-sky-400`) — swept per-file under D21, not as a separate migration.
|
||||
173
docs/redesign-warm-hive/PR7-BUILD-PLAN.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Warm-Hive PR7 — Auth (Clerk, themed · screen 13) · Billing (Stripe, themed · screen 14) — Build Plan
|
||||
|
||||
> Source design: `docs/design_handoff_waggle_app/SCREENS.md §13` (Auth) + `§14` (Billing) + `design-files/screens/{auth,billing}.html`. Roadmap: `BUILD-PLAN.md §6` (PR7 = screens 13·14). Strategic gate: `DESIGN_POV.md §4` (BYO-vs-metered) + `BUILD-PLAN.md §7.5`. Branch: TBD off main @ `3764bc13` (PR1–PR6 all shipped). Status: **FOUNDER-RATIFIED 2026-06-18 — D1=Option A (BYO-key + flat subscription); scope=PR7a Billing first; Clerk=optional sign-in (b), key forthcoming. Building PR7a now; PR7b deferred to next arc (needs Clerk key).** D5–D17 proceed as recommended.
|
||||
> Recon: 7-reader grounded recon (`pr7-recon/01..07`), every claim verified vs live code with file:line. Corrections + drift in §8 (honesty log).
|
||||
> **The single most important truth in this plan:** unlike PR3–PR6, PR7 renders *identity and money*. Real auth and real payments **cannot be faked** — every fabrication trap (a logged-in user that isn't real, invented invoices, hardcoded payment methods, fake "next charge" dates, a custom card form that doesn't tokenize) is gated off in §5. **PR7 is the highest fabrication-risk PR of the redesign.**
|
||||
|
||||
---
|
||||
|
||||
## 0. STRATEGIC GATE — BYO-key vs Waggle-metered (settle this FIRST, before any feature code)
|
||||
|
||||
`DESIGN_POV §4` (`DESIGN_POV.md:62-70`) flagged "who pays for inference (BYO-key vs Waggle-metered)" as the decision that "quietly reshapes Billing, Onboarding, and Usage" and "must be settled before Billing goes live" (`DESIGN_POV.md:89-90`; `BUILD-PLAN.md:165-166`). It was deferred through PR5 (where D1 ratified BYO-key for the model gate) and now becomes the literal precondition for PR7.
|
||||
|
||||
**The de-facto current commitment is Option A — and it is already shipped, wired, and tested end-to-end:**
|
||||
- **BYO-key is the only inference-payment path that exists.** The shared `ModelGate` copy is literally *"Bring your own key — it's stored encrypted in your Vault and never leaves your machine"* (`ModelGate.tsx:186`), live-validated then written to Vault (`ModelGate.tsx:94-99`); onboarding hard-gates Continue on a working model (`ModelGateStep.tsx:49`). Waggle never holds an inference key or pays a provider on the user's behalf in any shipped path.
|
||||
- **Stripe is flat `mode:'subscription'`** on both the local sidecar (`checkout.ts:40`, `line_items` qty 1, fixed price, PRO/TEAMS only) and `apps/www` (`checkout/route.ts:170`). Two products (Pro $19, Teams $49/seat), monthly+annual.
|
||||
- **There is ZERO inference-metering plumbing.** No `createUsageRecord` / `billing_meter` / `reportUsage` / credits ledger anywhere in `packages/server/src` (grep → 0; only a code comment at `local/index.ts:1980`). The only "usage" surface is `TelemetryApp` ("Usage & cost") reading the **estimate-only** `/api/cost/summary` with an **advisory** 80%/exceeded budget warning that **enforces nothing** (`cost.ts:162-169`) — a dashboard, not a quota.
|
||||
|
||||
| | **Option A — Ratify BYO-key + flat subscription** | **Option B — Pivot to Waggle-metered** |
|
||||
|---|---|---|
|
||||
| Build cost | **Near-zero new backend.** PR7 Billing = theming over the working subscription flow + minor wiring (annual toggle, deep-link, success route). | **Multi-month strategic arc.** New per-request metering, durable usage ledger, hard quota enforcement on the inference/agent-loop path, Stripe metered prices + new webhook events, a credits/balance surface, a managed key pool (the `managedModelPool` flag has no inference-path impl today). |
|
||||
| Positioning | Consistent with the shipped local-first "your key never leaves your machine" promise. | **Contradicts** that promise; adds inference COGS + abuse surface; inverts the onboarding model gate. |
|
||||
| Blast radius | PR7 theming only. | Billing + Onboarding (gate inverts) + Usage (full `TelemetryApp` rebuild) + core inference path + Stripe webhook + product copy. |
|
||||
|
||||
**Recommendation: ratify Option A.** The codebase already committed end-to-end; Option B is a product pivot PR7 must not silently absorb. The Plans copy "Memory is free forever. You only pay for scale — no feature-count games." (`billing.html:130-131`) *leans* metered but is honestly satisfiable under A (Pro/Teams unlock scale: workspaces, marketplace, sync). **This is a founder strategic call, not a build choice — it must be answered before §3 / any code.**
|
||||
|
||||
---
|
||||
|
||||
## 1. The design contract
|
||||
- **§13 Auth** (`auth.html`): split grid (1.05fr brand panel / 1fr form, brand panel hidden <820px, `auth.html:20-22,73,89-152`). **4 states** — *Sign in* (Google+Apple SSO + email/pw + "Forgot?"), *Sign up* (honey local-note "You don't need this to start." + Name/Email/Password "At least 10 characters"), *Verify* (6-box OTP, auto-advance + backspace-to-prev, `auth.html:163-169`), *SSO/enterprise* (work-email field + neutral "SAML, SCIM provisioning, audit logs available on Teams and KVARK · Talk to sales →"). Load-bearing trust copy: *"An account is optional — Waggle runs fully local without one"* + *"Your memory stays yours; sign-in only adds sync"* (`auth.html:98-99`). Design note `SCREENS.md:277`: "Build with **Clerk** components themed to the tokens." The top `.controls` segmented switcher (`auth.html:78-87`) is **concept-harness scaffolding for previewing states — NOT product UI**.
|
||||
- **§14 Billing** (`billing.html`): segmented **Plans / Checkout / Success / Manage** (`billing.html:117-122`). Plans: H1 "Upgrade your hive.", Monthly|Annual −20% toggle, 3 cards (Solo $0 "Current" disabled · Pro $19/mo honey "Most popular" → "Choose Pro" · Teams $49/seat → "Choose Teams"). Checkout: a *mock* in-app card form + order summary. Success: "You're Pro." + a receipt block. Manage: current plan + payment method + invoices (Paid+PDF) + change/cancel. Governing instruction `SCREENS.md:294`: **"Use Stripe Checkout/Customer Portal where possible; theme to tokens."** The card form and invoice table are **fidelity mockups of Stripe's hosted surfaces, not a spec to hand-build.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Current state — **grounded, the two-stack split is the master fact** (verified vs live, 2026-06-18)
|
||||
|
||||
**THE master fact (read before anything):** there are **two** server stacks and PR7 builds in `apps/web` → the **LOCAL sidecar** (`packages/server/src/local/index.ts`), which is **config.json-tier and has NO Clerk auth**. The rich Clerk-linked Stripe flow + real Clerk UI live **only** in `apps/www` (Next.js, PR8) + the cloud entry `packages/server/src/index.ts`. **Do not conflate them.** (`recon 01 §0`, `recon 02 §0`, `recon 05 §1-3`, `recon 03 §0`.)
|
||||
|
||||
| Layer | Reality today (file:line) | State |
|
||||
|---|---|---|
|
||||
| **Local "auth"** | Per-process random bearer token `wsSessionToken = crypto.randomBytes(32)` minted at sidecar boot (`local/index.ts:1409`), served auth-exempt+same-origin via `GET /api/auth/session-token` (`local/index.ts:2047-2051`), enforced by **token equality, no user lookup** (`security-middleware.ts:340-377`). Adapter attaches `Authorization: Bearer` + 401→refresh→retry for process rotation (`adapter.ts:298-337,444-470`). **This IS the design's "account is optional, runs fully local."** | **REAL (not a login)** |
|
||||
| **Clerk in `apps/web`** | **ZERO.** grep `@clerk` in `apps/web/package.json` → none; `ClerkProvider`/`SignIn`/`useSignIn` across `apps/web/src` → 0 files. No `/auth` route. Clean slate. | **ABSENT** |
|
||||
| **Clerk elsewhere (REAL, not PR7's surface)** | Cloud server verifies Clerk JWTs (`plugins/auth.ts:3,21,31-48` verifyToken→`users` table keyed by `clerkId`); `apps/www` has full themed Clerk UI (`<SignIn/>` catch-all, `ClerkProvider` + Hive appearance map, `apps/www/app/layout.tsx:6-93`). Local sidecar references Clerk only as `isTeamMode = !!process.env.CLERK_SECRET_KEY` to toggle a session timeout (`security-middleware.ts:307-309`). | **REAL (cloud/www only)** |
|
||||
| **Desktop "identity"** | Free-text **name** from IdentityLayer (onboarding), surfaced as `HomeBriefing.userName` (`home.ts:261-270`) + sidebar row via `adapter.getIdentity()` degrading to "Account"/"W" when blank (`AppShell.tsx:98-104,321`). **Identity, NOT auth** — must never render as "signed in." | **REAL (identity ≠ account)** |
|
||||
| **Stripe backend (local sidecar)** | `stripeRoutes` registered (`local/index.ts:128,2161`); all gate on `STRIPE_SECRET_KEY` → **503 `STRIPE_NOT_CONFIGURED`** when absent (`stripe/index.ts:25-43`). `create-checkout-session` REAL (hosted Checkout, `mode:subscription`, PRO/TEAMS, period-aware, returns `{url}`, `checkout.ts:16`). `create-portal-session` REAL (hosted Customer Portal, `requireTier('PRO')`, needs `config.json.stripe_customer_id` else 400 `NO_STRIPE_CUSTOMER`, `portal.ts:15,32`). `sync` REAL + payment-gated (`sync.ts:46-49`), returns only `{tier,customerId}` (`sync.ts:83`). Webhook **flips tiers** (payment-gated, signature-verified, idempotent, TOCTOU-serialized; 17/17 tests green per CLAUDE.md §10 E-10; `webhook.ts:114-158`). | **REAL** |
|
||||
| **No invoice / payment-method / sub-detail routes** | grep `invoice`/`paymentMethod`/`invoices.list`/`paymentMethods` in `packages/server/src` → **0 product hits**. | **MUST-BUILD or defer to Portal** |
|
||||
| **Billing FE** | Fully wired: `adapter.createCheckoutSession/createPortalSession/syncStripeCheckout/getTier` (`adapter.ts:2658-2685`) + `useBilling` hook orchestrates the happy path incl. `?session_id=` auto-sync (`useBilling.ts:104-115`) + the `tierResolved` honesty guard (`useBilling.ts:16-46`). Settings → **"Plan" tab** is already the honest Manage surface: real tier badge, upgrade buttons → `startCheckout`, "Manage Subscription" → `openPortal` (`SettingsApp.tsx:511-655`, `:621-635`). | **REAL** |
|
||||
| **Routing** | All 28 screens are children of one `<Route path="/" element={<AppShell/>}>` (`App.tsx:52-110`). No `/auth`, `/billing`, `/payment-success` routes (grep: 0). `SettingsApp` inits `activeTab='models'` with **no `?tab=` reader** (`SettingsApp.tsx:53`) → `/settings?tab=billing` silently opens Models. Breadcrumb label sourced only from `dock-tiers` via `matchNavRoute` (`AppShell.tsx:224-229`). | mixed |
|
||||
| **Price env contract** | Dual: 4-var (`STRIPE_PRICE_PRO_MONTHLY/_ANNUAL/_TEAMS_*`) + legacy single-var, resolved by `tierFromPriceId`/`priceIdForTier` (`stripe/index.ts:71-100`). Live+test Stripe accounts hold the prices per CLAUDE.md §10 M7. | **REAL (EXTERNAL-DEP at runtime)** |
|
||||
|
||||
**Net:** **Screen 14 (Billing) is ~90% real** — a themed re-skin of the existing `useBilling`→Stripe-hosted flow + four small builds (annual toggle wiring, `?tab=` deep-link, `/payment-success` route, gated Success/Manage detail). **Screen 13 (Auth) is overwhelmingly net-new + EXTERNAL-DEP + decision-gated** — no Clerk SDK, no login UI, no real user session anywhere the desktop can reach.
|
||||
|
||||
### 2.5 Recommended sub-PR split (by risk shape — the core structural recommendation)
|
||||
Auth and Billing have **opposite risk shapes**: Billing is reskin-over-real with a localized blast radius and no new dependency; Auth is a net-new pre-shell route + an EXTERNAL Clerk-key dependency + an unproven Tauri-WebView OAuth question. Coupling them in one PR would block the de-risked Billing work behind the founder's Clerk-architecture decision and key provisioning. **Recommend a 2-way split, ordered by value/risk:**
|
||||
|
||||
| Sub-PR | Screen | Scope theme | Why this grouping | Effort |
|
||||
|---|---|---|---|---|
|
||||
| **PR7a — Billing themed over existing Stripe** | **14 Billing** | Reskin the Settings → Plan tab (and/or a standalone `/billing`) to the §14 4-state layout over the *already-real* `useBilling`→hosted-Checkout/Portal flow. Adds: annual-toggle wiring, `?tab=` deep-link, `/payment-success` route. **No new dependency.** | **Lower risk, ships standalone, gated only on the §0 BYO/metered call** (which is a copy decision under Option A, not a code blocker — the flow supports either). Hosted Checkout/Portal means **zero fabricated billing data**. | **S–M** |
|
||||
| **PR7b — Auth (Clerk) pre-shell route** | **13 Auth** | New top-level **sibling route outside AppShell** + (if Option b) `@clerk/clerk-react` provider themed to warm tokens, with a hard **no-key → local-first accountless** fallback. | **Higher risk + EXTERNAL-DEP-blocked** (Clerk publishable key) + an architectural decision (cloud-only vs optional-sign-in vs full-gate) + an unproven Tauri-WebView OAuth spike. Isolating it keeps the Clerk dependency + the pre-shell routing risk off Billing. | **M–L** (or **S** if cloud-only/handoff) |
|
||||
|
||||
**Sequencing:** PR7a first (de-risked, no dependency, near-zero new backend, ships the upgrade funnel). PR7b second (carries the Clerk dependency + the architecture call). Each is independently revertible. **If the founder defers the Clerk-architecture decision or the key, PR7a still ships alone** — Billing is not blocked by Auth.
|
||||
|
||||
---
|
||||
|
||||
## 3. Decisions — **NEED FOUNDER RATIFICATION before feature code** (recommend-and-proceed unless you object; D1/D2/D5 are strategic)
|
||||
|
||||
| # | Decision | Recommended | Why |
|
||||
|---|---|---|---|
|
||||
| **D1** | **§0 BYO-key vs Waggle-metered** (the strategic gate) | **Ratify Option A — BYO-key + flat subscription** | The codebase already committed end-to-end (`ModelGate.tsx:186`, `checkout.ts:40`, zero metering plumbing). Option B is a multi-month pivot that contradicts the shipped "your key never leaves your machine" promise. Founder strategic call; everything else assumes A. |
|
||||
| **D2** | **Clerk architecture for the desktop** (cloud-only vs optional-sign-in vs full-gate) | **Option (b): optional Clerk in the SPA, accountless default; sign-in unlocks sync/Teams/billing. REJECT full-gate.** | The only option consistent with the design copy ("account is optional", `auth.html:98`), the accountless sidecar (`local/index.ts:2047-2052`, `settings.ts:309-331`), the existing `useBilling` tier flow, and the prior ratified "Tauri Clerk = Phase 2 fast-follow, NOT Day 0" (brief 2026-05-03:225). Full-gate (c) breaks accountless boot — do not build. Founder strategic call (governs the whole-SPA provider wrapping in `main.tsx`). |
|
||||
| **D3** | **Sub-PR split** (§2.5) | **Ship the 2-way split: PR7a (Billing) → PR7b (Auth)** | Opposite risk shapes; coupling blocks de-risked Billing behind the Clerk decision + key. Billing has no new dependency; Auth is EXTERNAL-DEP + architecture-gated. Independently revertible. Founder structural call. |
|
||||
| **D4** | **Auth surface scope** — browser/cloud-first vs in-WebView Tauri sign-in now | **Browser/cloud-first; treat in-WebView Tauri sign-in as an explicit later deep-link/OAuth spike** | Clerk hosted OAuth/redirect inside a Tauri WebView is **unproven** and the prior brief deferred it (brief:225); the `apps/www` browser pattern is proven. Determines whether PR7b must also solve Tauri deep-link OAuth (large, risky) or reuse the proven browser provider (small). |
|
||||
| **D5** | **Checkout UI** — custom in-app card form vs hosted Stripe Checkout | **Hosted Stripe Checkout redirect (reuse `createCheckoutSession`); theme the "Checkout" segment as an order-summary that hands off** | `SCREENS.md:294` mandates "use Stripe Checkout where possible"; the backend only emits a hosted URL (`checkout.ts:39-51`); `apps/web` has **zero** Stripe.js/PaymentElement (grep 0). A real PAN field raises PCI scope SAQ-A → SAQ-A-EP for zero functional gain. The `billing.html` card form is a mockup of Stripe's page, not a spec. **Both a fabrication trap and a PCI trap — do not hand-build.** |
|
||||
| **D6** | **Manage state** — in-app invoice/payment-method/cancel vs hosted Customer Portal | **Hosted Customer Portal launchpad (reuse `createPortalSession`); themed current-plan header in-app, all mutations + invoices + payment method via Portal** | Portal natively covers payment-method update, invoice PDFs, plan change, cancel. No invoice/payment-method/sub-detail route exists (grep 0). `SettingsApp.tsx:621-635` already does exactly this. Building in-app Stripe write-APIs is scope+fabrication risk we don't need. (If the founder *explicitly* wants in-app rendering: build new sidecar routes against `stripe.invoices.list`/`paymentMethods.list` and render **strictly from live data** — never placeholders.) |
|
||||
| **D7** | **Billing registration** — standalone `/billing` vs Settings "Plan" tab | **Keep the Settings → Plan tab as primary (it's fully wired to real Stripe); reskin it to the §14 layout + add a `?tab=` reader. Add a standalone `/billing` (PR6 wrapper pattern, AppShell child) only if design wants the full-screen funnel outside Settings chrome — reusing `useBilling` verbatim** | The Plan tab is real and tier-honest today. A `?tab=billing` deep-link inherits the "Settings" breadcrumb automatically (`dock-tiers.ts:108`). Standalone is optional and must add its own `dock-tiers` route+label entry (don't repeat PR6's label-less surfaces). |
|
||||
| **D8** | **Monthly/Annual −20% toggle** — honor at checkout vs display-only | **Build it: thread `billingPeriod` through `adapter.createCheckoutSession` + `useBilling.startCheckout`** | Backend `priceIdForTier(tier, 'annual')` already resolves the annual price (`stripe/index.ts:91-100`) but the adapter drops the arg (`adapter.ts:2667`), so it always charges monthly. A toggle that visibly changes price but charges monthly is a **trust/billing bug**. Small FE change; needs `STRIPE_PRICE_*_ANNUAL` envs set (EXTERNAL-DEP). |
|
||||
| **D9** | **Success state** — render the receipt block vs confirmation-only | **Confirmation off the synced tier only ("You're Pro"); GATE OFF the receipt rows; drive any trial line from real `trialDaysRemaining`** | `sync` returns only `{tier,customerId}` (`sync.ts:83`) — the receipt #, "Trial ends Jun 28", "Then $19", "Emailed →" have **no data source** and would be fabricated. The post-redirect `/payment-success` URL is real (`checkout.ts:42`) but needs a route (D11). |
|
||||
| **D10** | **Plan-card prices** — hardcode copy vs machine-readable source | **Hardcode the display copy ($0/$19/$49-seat, $15/$39 annual) from `tiers.ts:7-12` in ONE constant, treat as copy not fact; never invent per-seat math beyond what `tiers.ts` documents** | Only `stripePriceId` is a real field in `TIER_CAPABILITIES`; the dollar amounts are doc-comment copy. The *charged* amount/tax must come from Stripe's hosted page, never asserted by our UI. Prices match `billing.html` exactly today. |
|
||||
| **D11** | **`/payment-success` (+ `/payment-cancelled`) route** | **Add `/payment-success` as an AppShell child (user is back inside the app post-checkout) using the PR6 wrapper pattern; reuse `useBilling`'s `?session_id=` auto-sync** | `checkout.ts:42` sets `success_url=/payment-success?session_id=…` but no such route exists (grep 0); today `?session_id=` is only read on whatever page is mounted. Small MUST-BUILD. |
|
||||
| **D12** | **`?tab=` deep-link reader in `SettingsApp`** | **Add a small `?tab=` initializer (read once on mount, snap `activeTab`); retarget ⌘K "Upgrade to Pro"/"Settings" + `UpgradeModal`/`TrialExpiredModal` fallbacks to `/settings?tab=billing`** | `SettingsApp` hardcodes `activeTab='models'` with no reader (`SettingsApp.tsx:53`); the existing `routes.ts:52` backup deep-link is already broken the same way. Without it every upgrade entry lands on Models, not Plan. |
|
||||
| **D13** | **OTP fidelity** (PR7b) — themed prebuilt Clerk vs bespoke 6-box widget | **Themed prebuilt `<SignIn/>`/`<SignUp/>` for v1 (accept Clerk's built-in code step); upgrade to `useSignIn` custom flow only if review demands the exact 48×56 honey OTP boxes** | `SCREENS.md:277` literally says "Build with Clerk components themed to the tokens." Prebuilt = small; custom flow = a whole hand-built sign-in/verify state machine with its own error handling + a11y. |
|
||||
| **D14** | **Clerk appearance** (PR7b) — shadcn theme vs full manual map | **shadcn theme (`@clerk/themes` shadcn) auto-reading the warm shadcn vars PR1 repointed (`index.css:20,29,46`) + a thin `variables` override for honey + Hanken; copy the `as const` gotcha avoidance from `apps/www/layout.tsx:31-34`** | `components.json` exists, so theming is mostly automatic + token-driven. A full manual hex map duplicates ~60 lines/component and drifts from the token system. Copy the *pattern*, not the *cooler `apps/www` hex*. |
|
||||
| **D15** | **SSO/enterprise panel** (PR7b) — live SAML vs sales CTA | **Keep it a custom "Talk to sales → Teams/KVARK" CTA; do NOT implement live SAML/SCIM** | The design panel (`auth.html:148`) is a sales CTA, not a live form; SAML/SCIM/audit are Teams/KVARK/Clerk-Enterprise features. Matches the KVARK funnel. |
|
||||
| **D16** | **EXTERNAL-DEP provisioning** | **Founder provides `VITE_CLERK_PUBLISHABLE_KEY` (reuse the existing `apps/www` instance key) + confirms Google/Apple social connections in the Clerk dashboard; confirms the sidecar runtime env carries `STRIPE_SECRET_KEY` + the 4 price IDs + `STRIPE_WEBHOOK_SECRET`** | Without the Clerk key, PR7b degrades to the accountless local-first state (honest). Without the Stripe envs, all `/api/stripe/*` return 503 and Billing must render an honest "not configured" disabled state. See §6. |
|
||||
| **D17** | **Warm-token sweep** | **In every sub-PR, sweep the touched files** — Auth/Billing must use `--honey`/`--surface`/`--line-strong`/`--r` from `waggle.css` (already landed by PR1, `BUILD-PLAN §3.1`); honey button-fg `#1a1407` = PR1's `--primary-foreground` | Both screens are heavily token-dependent; one pass keeps the "single honey accent" honest. No new tokens needed. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
### PR7a (Billing — reskin over REAL Stripe; DO NOT recreate `useBilling`, the adapter Stripe layer, or the server `stripe/*` routes)
|
||||
- **Reskin** `SettingsApp.tsx:511-655` (Plan tab) to the §14 segmented **Plans / Checkout / Success / Manage** layout, warm tokens (D17). Reuse the existing tier card + `useBilling` + `tierResolved` honesty guard verbatim.
|
||||
- **Plans:** 3 cards from `tiers.ts:7-12` copy (D10) + Monthly/Annual toggle (D8); current-plan marker from `useBilling().tier` honoring `tierResolved`; "Choose Pro/Teams" → `startCheckout(tier, period)`.
|
||||
- **Checkout segment:** themed order-summary that redirects to **hosted Stripe Checkout** (D5) — no in-app PAN field.
|
||||
- **Success:** confirmation off synced tier (D9); receipt block gated off.
|
||||
- **Manage:** themed current-plan header + single "Manage via Stripe" → **hosted Customer Portal** (D6); no in-app invoice/payment-method rows.
|
||||
- **Wiring (the small builds):** thread `billingPeriod` through `adapter.createCheckoutSession` (`adapter.ts:2667`) + `useBilling.startCheckout` (D8); add `?tab=` reader to `SettingsApp` (D12); add `/payment-success` AppShell child route via the PR6 wrapper pattern (D11); retarget ⌘K + modal fallbacks (D12). **Optional** standalone `/billing` (D7) follows the PR6 `BenchmarkRoute.tsx:1-11` pattern (AppShell child).
|
||||
- **Backend:** none under Option A (D1) + hosted Checkout/Portal (D5/D6). Only if the founder picks in-app Manage rendering do new `invoices.list`/`paymentMethods.list` sidecar routes land → `tsc -p packages/server`.
|
||||
|
||||
### PR7b (Auth — net-new pre-shell route + optional Clerk; the `clerk-setup`/`clerk-react-patterns`/`clerk-custom-ui`/`clerk-billing` skills apply)
|
||||
- **`/auth` is the ONE pre-shell route** — register as a **top-level sibling OUTSIDE `<Route path="/" element={<AppShell/>}>`** (`App.tsx:52`), no Sidebar/StatusBar/ChatHost/boot gate. `ThemeProvider` already wraps `<BrowserRouter>` (`App.tsx:53`) so warm tokens apply outside the shell. Use a plain `AppErrorBoundary` (`onClose → window.location.reload()`), **NOT `SurfaceBoundary`** (its `onClose → /home` assumes an authenticated home, `SurfaceBoundary.tsx:13`). No ⌘K entry (pre-login). No breadcrumb needed (renders outside AppShell, so StatusBar never mounts).
|
||||
- **Custom layout (MUST-BUILD, no backend):** split brand panel + pitch + trust lines + local-first banner + enterprise→sales CTA (D15). All copy verbatim in `recon 03 §2/§4`.
|
||||
- **Clerk (Option b, D2):** add `@clerk/clerk-react` + `@clerk/themes`; wrap `<App/>` in `main.tsx:18` with a **no-key guard** — if `import.meta.env.VITE_CLERK_PUBLISHABLE_KEY` is undefined, render children WITHOUT `ClerkProvider` and show the accountless local-first state (never crash, never fabricate a user). Themed via the shadcn appearance approach (D14). Prebuilt `<SignIn/>`/`<SignUp/>` themed (D13); `<UserButton/>`/`useUser()` may re-feed the sidebar `userName` row (currently `null`→"Account").
|
||||
- **`getToken()` seam:** `useAuth().getToken()` returns the Clerk JWT in exactly the `Bearer` shape the adapter (`adapter.ts:445`) + cloud `plugins/auth.ts:31` already consume — an *optional* sign-in can swap the device token for a user JWT **only when the user opts into cloud/Teams**, leaving the accountless local path untouched. Do NOT make Clerk the API authorizer on the local path (regresses local-first).
|
||||
|
||||
---
|
||||
|
||||
## 5. No-fabrication contract — every place PR7 could invent identity or money (gated off)
|
||||
|
||||
PR7 renders identity and money. Each item below has **no honest data source today** and **must be gated off, never invented** (carry the PR3/PR3.5 `tierResolved`/`userName=null` precedent):
|
||||
|
||||
| # | Fabrication trap | Why it's fake | Gate (the honest behavior) |
|
||||
|---|---|---|---|
|
||||
| **F1** | **A logged-in identity that isn't real** (name/email/avatar, "Signed in as…") | The desktop has no Clerk session; "identity" is a memory-derived IdentityLayer name (`AppShell.tsx:98-104`). | With no `VITE_CLERK_PUBLISHABLE_KEY`, render the **accountless local-first** state; the sidebar `userName=null`→"Account"/"W" fallback is the accepted honest UI (BUILD-PLAN §9). Only populate from a **real** `useUser()`. |
|
||||
| **F2** | **Fake invoices / PDF receipts** (Manage: "Invoice · Paid · PDF", `billing.html`/`SCREENS.md:292`) | No invoice route exists (grep 0). | Route to the hosted **Customer Portal** (`openPortal`); never render invented invoice rows or fake PDF links. No customer → empty/portal state, not stub rows. |
|
||||
| **F3** | **Fake payment method ("VISA ···4242")** (`SCREENS.md:291`) | The `4242` is literally Stripe's **test PAN**, design filler; no payment-method route. | Render only from live `stripe.paymentMethods.list` (if D6 in-app path) else **Portal-only**; never hardcode a masked card. |
|
||||
| **F4** | **Fake "next charge" / renewal / billing-cycle date** ("renews Jul 14", `SCREENS.md:291-292`) | Not returned by `sync` (`sync.ts:83`) or `getTier`. | Derive from a real subscription fetch or **omit**; trial dates only via real `trialDaysRemaining`/`trialStartedAt`. |
|
||||
| **F5** | **Itemized Success receipt** ("$19 charged", receipt #, "Emailed →", `billing.html`) | `sync` returns only `{tier,customerId}`. | Generic "You're Pro" confirmation off the synced tier; receipt rows gated off (D9). |
|
||||
| **F6** | **Custom in-app card form** (email/card 4242/expiry/CVC) | Backend only emits a hosted URL; no Stripe.js in `apps/web` (grep 0). A real-looking PAN that doesn't tokenize is fake AND a PCI-scope trap (SAQ-A → SAQ-A-EP). | **Never collect raw PAN in-app**; redirect to hosted Stripe Checkout (D5). |
|
||||
| **F7** | **Tier shown as FREE before resolution** | `useBilling` defaults to `'FREE'` as a placeholder, not a fact. | Honor `tierResolved` (`useBilling.ts:16-46`) — render the unresolved state, not the FREE upgrade grid, until a real `getTier()` round-trip succeeds. |
|
||||
| **F8** | **A working checkout when Stripe is unconfigured** | All `/api/stripe/*` 503 without `STRIPE_SECRET_KEY` (`index.ts:25-43`). | Render an honest "not configured" disabled state, not a clickable fake "Subscribe". |
|
||||
| **F9** | **Fake annual price** (client-side `$19 × 0.8`) | The charged amount must match Stripe. | The −20% toggle must resolve through the real annual price var (`priceIdForTier(tier,'annual')`, D8), not a cosmetic client number; the hosted page renders the true amount/tax. |
|
||||
| **F10** | **Fake SSO success** ("Signed in with Google") + **fake OTP verification** | The `auth.html` demo advances on any input (`auth.html:138,164`); SSO buttons "succeed" with no provider. | SSO/OTP must perform a **real Clerk redirect/verification**; with no Clerk wired, disable / route to `apps/www` / show the local-first path — never simulate a session. |
|
||||
| **F11** | **SAML/SCIM rendered as a live form** | Enterprise panel names SAML/SCIM/audit (`auth.html:148`) — Teams/KVARK/Clerk-Enterprise features. | Keep a **sales CTA** (D15); do not render a SAML form that does nothing. |
|
||||
| **F12** | **Usage/credits balance** ("$N remaining", "X of Y tokens") | Only relevant if Option B; no ledger exists, `/api/cost/summary` is **estimate-only** + **advisory** (`cost.ts:162-169` enforces nothing). | Under Option A this surface isn't built; the estimate dashboard must not be dressed up as a metered balance or imply requests are capped. |
|
||||
| **F13** | **Hardcoded demo identity** (`mara@egzakta.com` / "Mara Kovač", `auth.html:113,127,136`; `billing.html` name-on-card) | Pure mock placeholders. | Never pre-fill or display the demo user; no real account email exists pre-Clerk. |
|
||||
|
||||
---
|
||||
|
||||
## 6. External dependencies — founder must supply (EXTERNAL-DEP blockers)
|
||||
|
||||
| Dep | Needed by | Blocker behavior if absent (the honest fallback) |
|
||||
|---|---|---|
|
||||
| **`VITE_CLERK_PUBLISHABLE_KEY`** (reuse existing `apps/www` instance key) | PR7b Clerk SPA sign-in | No key → `/auth` degrades to the **accountless local-first** state ("continue without an account → Home"); never a fake identity (F1/F10). Desktop default path needs **no key**. |
|
||||
| **Google/Apple social connections** enabled in the Clerk dashboard | PR7b SSO buttons | Absent → SSO buttons disabled / "coming soon" / route to `apps/www`; never fake a session (F10). |
|
||||
| **`STRIPE_SECRET_KEY` + 4 price IDs (`STRIPE_PRICE_{PRO,TEAMS}_{MONTHLY,ANNUAL}`) + `STRIPE_WEBHOOK_SECRET`** in the sidecar runtime env | PR7a checkout/portal/sync/webhook | Absent → all `/api/stripe/*` return **503 `STRIPE_NOT_CONFIGURED`**; Billing renders an honest "not configured" disabled state (F8). Live+test accounts already hold the prices (CLAUDE.md §10 M7) — the secret + IDs must be present at runtime. |
|
||||
| **`config.json.stripe_customer_id`** (written only after a real paid checkout/webhook, `webhook.ts:60`) | PR7a Manage → Portal | Absent → `create-portal-session` returns **400 `NO_STRIPE_CUSTOMER`** (`portal.ts:32`); Manage must show the pre-subscription state, not a broken portal jump. |
|
||||
| **§0 BYO-vs-metered decision (D1)** | Plans copy + whether any Usage/metering surface is implied | Unsettled → do not write the Plans/Usage copy that commits to metered; the flow supports either but the positioning must be chosen. |
|
||||
| **Clerk architecture decision (D2)** + **Tauri-WebView OAuth spike (D4)** | PR7b shape | Unsettled → PR7b cannot start; PR7a still ships independently. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Phased plan (TDD; commit per phase; FE `tsc -p apps/web/tsconfig.app.json` + vitest each)
|
||||
|
||||
**PR7a — Billing** (gated on D1 copy decision; no new dependency)
|
||||
- **A1 — Wiring prerequisites.** `?tab=` reader in `SettingsApp` (D12); thread `billingPeriod` through adapter + `useBilling.startCheckout` (D8); add `/payment-success` AppShell child route (D11); retarget ⌘K + `UpgradeModal`/`TrialExpiredModal` fallbacks (D12). Unit tests (deep-link snaps to Plan; checkout sends period; success route auto-syncs on `?session_id=`).
|
||||
- **A2 — Plan tab reskin.** §14 Plans/Checkout/Success/Manage segmented layout over `useBilling`; warm tokens (D17); 3 cards + annual toggle (D10/D8); Checkout = hosted-redirect summary (D5); Success = confirmation only, receipt gated (D9/F5); Manage = current-plan header + Portal launchpad (D6/F2/F3/F4); honor `tierResolved` (F7) + 503 "not configured" state (F8). Extend `SettingsApp` tests (renders unresolved state; gated receipt/invoice/payment-method; honey "Most popular").
|
||||
- **(optional) A3 — Standalone `/billing`** (D7) only if design needs the full-screen funnel — PR6 wrapper pattern, reuse `useBilling`, add a `dock-tiers` route+label entry.
|
||||
|
||||
**PR7b — Auth** (gated on D2 architecture + D16 Clerk key; can defer/ship cloud-only)
|
||||
- **B1 — Pre-shell `/auth` route + custom layout.** Sibling route outside AppShell (`AppErrorBoundary`, not `SurfaceBoundary`); split brand panel + pitch + trust lines + local-first banner + enterprise sales CTA (D15); all copy verbatim. Unit test (renders outside shell; no fabricated identity with no key — F1/F13).
|
||||
- **B2 — Clerk provider + themed components** (Option b). `@clerk/clerk-react` + `main.tsx` wrap with no-key guard; shadcn appearance + warm override (D14); prebuilt themed `<SignIn/>`/`<SignUp/>` (D13); SSO/OTP perform real Clerk flows or degrade honestly (F10/F11). Optional `getToken()`→Bearer cloud-sync seam (do not make Clerk the local API authorizer). Tests (no-key → accountless; real `useUser()` only).
|
||||
|
||||
**Each sub-PR ends with:** adversarial review (correctness / security / honest-identity-and-money / design-fidelity) + live smoke (dark + light, 0 console errors).
|
||||
|
||||
---
|
||||
|
||||
## 8. Honesty log — every drift the recon found (verified vs live)
|
||||
|
||||
- **The two-stack split is the master correction.** PR7 builds in `apps/web` → the **local sidecar** (config.json tier, **no Clerk**). The rich Clerk-linked Stripe flow + real Clerk UI are **only** in `apps/www` + the cloud server — PR8 territory, not reachable from the desktop. Conflating them would falsely assume Clerk identity + lazy-customer billing are "already available." (`recon 01 §0`, `recon 02 §0`, `recon 03 §0`, `recon 05 §1-3`.)
|
||||
- **"Auth" today is a per-process machine bearer token, not a login.** `wsSessionToken` (`local/index.ts:1409`) enforced by token equality (`security-middleware.ts:340-377`); the only "session lifecycle" is a silent 401→refresh for process rotation (`adapter.ts:316-337`). There is no signin/signup/logout anywhere in `apps/web`. `LoginBriefing.tsx` is the **overnight-work overlay**, not auth.
|
||||
- **Desktop "identity" ≠ account.** IdentityLayer name (`AppShell.tsx:98-104`) is a memory record typed in onboarding — it must **never** render as "signed in" (F1).
|
||||
- **Billing is ~90% real and tier-honest already.** `useBilling` + adapter + `stripe/*` all wired; the Settings Plan tab already does FREE/TRIAL→`startCheckout` and PRO/TEAMS→`openPortal` with the `tierResolved` guard. Screen 14 is a reskin, not a rebuild — the biggest risk is mistaking the real flow for net-new.
|
||||
- **The `billing.html` card form + invoice table + "VISA ···4242" + receipt rows are fidelity MOCKUPS of Stripe's hosted surfaces**, not a spec. `SCREENS.md:294` says "use Stripe Checkout/Customer Portal where possible." A custom PAN form is both fabrication (F6) and a PCI-scope jump.
|
||||
- **The annual toggle is a real FE build gap, not a styling gap.** Backend `priceIdForTier(tier,'annual')` resolves the annual price (`stripe/index.ts:91-100`) but the adapter drops `billingPeriod` (`adapter.ts:2667`) → the design's −20% toggle would visibly change the price label while charging monthly (a billing bug) unless wired (D8/F9).
|
||||
- **`/settings?tab=billing` silently opens Models.** `SettingsApp` hardcodes `activeTab='models'` with no `?tab=` reader (`SettingsApp.tsx:53`); the existing `routes.ts:52` backup deep-link is already broken the same way. Upgrade buttons currently land on Models, not Plan (D12).
|
||||
- **`/payment-success` is a dangling success_url.** `checkout.ts:42` redirects to a route that doesn't exist (grep 0); today `?session_id=` is only read on whatever page happens to be mounted (D11).
|
||||
- **DESIGN_POV said the design "supports either but commits to neither" (`DESIGN_POV.md:70`) — but the IMPLEMENTATION has since committed to Option A** (BYO-key + flat subscription) end-to-end. The §0 gate is now reconciling copy with shipped reality, not choosing from scratch.
|
||||
- **The `auth.html` top `.controls` segmented switcher is concept-harness scaffolding** for previewing the 4 states — NOT product UI. In the real build, state is route/Clerk-flow-driven.
|
||||
- **`@clerk/react` is in `node_modules` only as a transitive dep of `apps/www`'s `@clerk/nextjs`** — NOT an `apps/web` dependency. PR7b must add `@clerk/clerk-react` explicitly. The correct skill is `clerk-react-patterns` (RR6 SPA), **NOT** `clerk-react-router-patterns` (RR7 framework mode — does not apply, `recon 05 §10`).
|
||||
- **`apps/www`'s themed Clerk uses the OLD cooler Hive hex** (`#08090c`/`#e5a000`, `apps/www/layout.tsx:38-44`); copy the *pattern*, not the hex — `apps/web` is warm (`#14110b`/`#e9a52c`, `index.css:20,29`). Copy the `as const` gotcha avoidance (`layout.tsx:31-34`).
|
||||
- **No `KvarkNudge` component exists in `apps/web/src`** (grep 0; CLAUDE.md §9 references it but it isn't in the web app) — the Enterprise CTA is the inline `kvark.ai` link (`SettingsApp.tsx:644-651`); the Auth enterprise panel (D15) reuses that funnel as a sales CTA.
|
||||
</content>
|
||||
</invoke>
|
||||
144
docs/redesign-warm-hive/PR8-BUILD-PLAN.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Warm-Hive Redesign — PR8 Build Plan (`apps/www` landing → warm-Hive identity)
|
||||
|
||||
> Source design package: `docs/design_handoff_waggle_app/` — esp.
|
||||
> `design-files/Waggle Landing.html` (648 lines, the canonical landing reference) +
|
||||
> `design-files/styles/waggle.css` (token master) + `README.md §9` (landing copy notes).
|
||||
> Roadmap slot: `BUILD-PLAN.md §6` — **PR8, the final roadmap item** (PR1–PR7 all shipped to origin/main @ `58f64f22`).
|
||||
> Branch: `feature/warm-hive-pr8`. Merge convention: `--no-ff` (PR5/PR6/PR7 pattern).
|
||||
|
||||
---
|
||||
|
||||
## 0. Strategic gate — scope fork (resolved by evidence, recommend-and-proceed)
|
||||
|
||||
PR8's slot in §6 reads "**full content + identity**." Recon resolved this:
|
||||
|
||||
- **Content already shipped.** `apps/www` is a complete Next.js 15 (App Router) landing with
|
||||
deliberate, current copy — `app/page.tsx:24-28` documents the live structure as the chosen
|
||||
**"N2 IA"** with "the committed flat copy" (hero = *"Be the expert. We'll be the AI."*).
|
||||
`messages/en.json` (311 lines) holds the full marketing copy, intentionally **evolved past**
|
||||
the older `Waggle Landing.html` reference (current = 5 pillars + comparison/wow beats; the
|
||||
reference HTML = 6 pillars + BYO-agent + self-evolving sections). The current copy is the
|
||||
newer direction and is committed.
|
||||
- **Identity is stale.** `app/globals.css:4-26` still carries the **pre-redesign cool palette**
|
||||
(`--hive-950:#08090c`, `--honey-500:#e5a000`, blue-grey hive greys, saturated `--status-ai
|
||||
#a78bfa`/`--status-healthy #34d399`) and **Inter** as the typeface — none of the warm-Hive
|
||||
identity PR1 applied to `apps/web`.
|
||||
|
||||
**Ratified scope (recommend-and-proceed unless founder objects):**
|
||||
**PR8 = identity reskin only.** Port the warm-Hive token values + Hanken Grotesk + honey-accent
|
||||
discipline + honeycomb/hex motifs onto the existing N2 landing. **Do NOT rewrite copy** to the
|
||||
older reference HTML. **Do NOT touch benchmark/proof claims** (honest-stats — see §5). Treat the
|
||||
reference HTML as the source of truth for *visual treatment*, `en.json` as the source of truth
|
||||
for *copy*.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design contract (what "warm-Hive" means for the landing)
|
||||
|
||||
From `waggle.css §7` (and as implemented in `apps/web` PR1):
|
||||
|
||||
- **Dark default** surfaces: `--bg #14110b · --surface #1f1a12` (warm graphite, replaces cool `#08090c`).
|
||||
- **Honey accent**: `#e9a52c` (replaces `#e5a000`), used *sparingly* — primary buttons, active nav,
|
||||
key metrics, focus rings, 1–2 highlight words per headline. Never decorative fills.
|
||||
- **Type**: **Hanken Grotesk** (display+body, 300–800; headings 600 at `-0.02..-0.03em`) +
|
||||
**JetBrains Mono** (mono labels). Replaces Inter.
|
||||
- **Desaturated semantics**: `--intel #b196dd` (was `#a78bfa`), `--healthy #6cb78c` (was `#34d399`).
|
||||
- **Motifs**: honeycomb `.05` opacity SVG texture (warm line stroke), hexagon clip-path for the W
|
||||
mark / persona tiles, warm shadows + honey glow.
|
||||
- **Dark-only** for the landing (current site has no light theme; the design specifies both, but
|
||||
adding a landing theme toggle is out of reskin scope → deferred, §8).
|
||||
|
||||
---
|
||||
|
||||
## 2. Current state (grounded, file:line)
|
||||
|
||||
| Fact | Evidence |
|
||||
|---|---|
|
||||
| Next.js 15.1 App Router, React 19, TS 5.9, **vanilla CSS (no Tailwind)** | `apps/www/package.json:14-24`, `globals.css:2` |
|
||||
| Tokens = old cool palette + Inter | `globals.css:4-40` |
|
||||
| Components style via inline `var(--hive-*)`/`var(--honey-*)` → value remap cascades | grep across `app/_components/*` |
|
||||
| **Hardcoded** hex (bypasses vars) — must fix explicitly | `layout.tsx:38-91` (Clerk), `HeroVisual.tsx:62-309` (SVG), `BrandPersonasCard.tsx:343-417` (`<style>`) |
|
||||
| Honeycomb SVG uses URL-encoded old hex `%231f2433` | `globals.css:54` |
|
||||
| Honey rgb literal `229,160,0` in glow/selection/shadow | `globals.css:21,22,25,48` |
|
||||
| Clerk + Stripe + next-intl wired; legal routes + sign-in/up + account exist | `layout.tsx`, `app/api/stripe/*`, `app/(legal)/*` |
|
||||
| Single FE test (`BrandPersonasCard.test.tsx`); vitest | `__tests__/`, `vitest.config.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Decisions (recommend-and-proceed)
|
||||
|
||||
| # | Decision | Choice | Why |
|
||||
|---|---|---|---|
|
||||
| D1 | Scope | **Reskin, not rebuild** | Content is current/committed (N2 IA, `page.tsx:24`); only identity is stale |
|
||||
| D2 | Token strategy | **Remap var VALUES 1:1 by scale stop**, keep var NAMES | Components reference vars → one cascade restyles all (PR1 method) |
|
||||
| D3 | Hardcoded hex | **Literal remap table** applied to `app/**/*.{tsx,ts,css}` | Catches Clerk/SVG/style-block hex + stale fallbacks in one auditable pass |
|
||||
| D4 | Font | **Hanken Grotesk + JetBrains Mono** via `next/font/google` | Design typeface; `next/font` keeps SSR/perf |
|
||||
| D5 | Light theme | **Defer** (keep dark-only) | Landing has no toggle today; adding one is out of reskin scope (§8) |
|
||||
| D6 | Proof/benchmark copy | **Untouched** | Honest-stats — conservative framing is deliberate; upgrading to 87.66 SOTA is a founder call |
|
||||
| D7 | Deploy/env (Clerk/Stripe prod keys, Vercel) | **Documented, not executed** | EXTERNAL-DEP, founder-provisioned at deploy (§6) |
|
||||
|
||||
### Token remap table (old cool → warm-Hive, verbatim from `waggle.css`/`apps/web` PR1)
|
||||
```
|
||||
hive 950 #08090c→#0e0c07 900 #0c0e14→#14110b 850 #11141c→#1a160f 800 #171b26→#1f1a12
|
||||
700 #1f2433→#272117 600 #2a3044→#4a4030 500 #3d4560→#6b6250 400 #5a6380→#948a73
|
||||
300 #7d869e→#c8bfa9 200 #b0b7cc→#d8cfba 100 #dce0eb→#ece3d0 50 #f0f2f7→#f6f1e4
|
||||
honey 600 #b87a00→#c07e16 500 #e5a000→#e9a52c 400 #f5b731→#f6c45a 300 #fcd34d→#f9d27e
|
||||
sem status-ai #a78bfa→#b196dd status-healthy #34d399→#6cb78c
|
||||
rgb honey 229,160,0 → 233,165,44
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture (do NOT recreate)
|
||||
|
||||
- Keep next-intl/Clerk/Stripe wiring, the hero A/B variant resolver infra, the `_data`/`_lib`
|
||||
modules, legal routes, sitemap, event taxonomy — all untouched by a reskin.
|
||||
- Reuse the existing inline-CSSProperties + global-CSS pattern. No Tailwind, no new CSS framework.
|
||||
- Named warm tokens (`--bg`,`--surface`,`--text`,`--line`, semantics) are *added* alongside the
|
||||
remapped `--hive-*`/`--honey-*` scales for future use, but components keep using the scale vars.
|
||||
|
||||
---
|
||||
|
||||
## 5. No-fabrication contract (F-traps gated off)
|
||||
|
||||
| F | Trap | Gate |
|
||||
|---|---|---|
|
||||
| F1 | Upgrading proof copy to 87.66 SOTA under cover of "reskin" | Benchmark/proof strings in `en.json` **left byte-identical** |
|
||||
| F2 | Inventing testimonials (`proof.human_quote` is intentionally empty) | Leave empty |
|
||||
| F3 | Claiming light theme works when not added | Don't add toggle; note as deferred |
|
||||
| F4 | Drifting tier prices/names while editing | Pricing copy untouched (color-only) |
|
||||
| F5 | "Deployed/live" claims | Smoke is local `next dev`; deploy is EXTERNAL-DEP, documented not executed |
|
||||
|
||||
---
|
||||
|
||||
## 6. EXTERNAL-DEP (founder-provisioned at deploy — not build blockers)
|
||||
|
||||
| Dep | Needed by | Absent behavior |
|
||||
|---|---|---|
|
||||
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` + secret | prod sign-in/up | Build/dev render; auth round-trip needs real keys |
|
||||
| Stripe live keys + 4 price IDs + webhook secret | prod checkout | `checkout` route already fails honestly without |
|
||||
| Vercel project + `waggle-os.ai` DNS | go-live | per `docs/launch/drafts/2026-05-12-apps-www-deployment-readiness.md` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Phased plan + gates
|
||||
|
||||
- **A — Tokens & type** (`globals.css`, `layout.tsx`): remap `:root` values, honeycomb stroke,
|
||||
honey rgb, font Inter→Hanken+JetBrains Mono, heading styles, Clerk appearance hex→warm.
|
||||
- **B — Hardcoded-hex sweep** (`HeroVisual.tsx`, `BrandPersonasCard.tsx`, page-level fallbacks,
|
||||
`methodology/page.tsx`): apply remap table; re-grep proves zero old-palette hex remains in `app/`.
|
||||
- **C — Verify**: `tsc --noEmit` (apps/www) 0 · `next build` green · `next lint` clean · `vitest run` green.
|
||||
- **D — Adversarial review** (workflow): design-fidelity/leak, honest-stats, security(keys), correctness, a11y/contrast. Fix HIGH+MEDIUM.
|
||||
- **E — Live smoke**: `next dev`, screenshot landing + sign-in, **0 console errors**; commit
|
||||
`smoke-pr8-<date>/REPORT.md` + PNGs.
|
||||
- **F — Ship**: merge `--no-ff`, push origin, handoff + MEMORY.md START HERE.
|
||||
|
||||
**Gate (before merge):** tsc 0 · build green · lint clean · vitest green · review HIGH/MEDIUM clear · smoke 0 console errors.
|
||||
|
||||
---
|
||||
|
||||
## 8. Honesty log / deferred
|
||||
- Landing stays **dark-only**; warm light-paper theme + a nav theme toggle deferred (design supports it; out of reskin scope).
|
||||
- Proof copy stays conservative (67.8% AND-of-3); upgrading to the now-public 87.66 SOTA headline is a founder content decision, not this PR.
|
||||
- Older `Waggle Landing.html` has 6 pillars + BYO-agent + self-evolving sections the live N2 IA folds/omits; reconciling content to the reference is a separate content PR, not this reskin.
|
||||
- `methodology/page.tsx` + page fallbacks updated for hex consistency though vars already cascade.
|
||||
301
docs/redesign-warm-hive/pr3-recon/chat.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# PR3 Recon — SCREEN 02 · Chat (ship Variation B "Split work canvas")
|
||||
|
||||
Branch: `feature/warm-hive-redesign`. Maps the **current Chat/agent-runtime** implementation
|
||||
in `apps/web` against `docs/design_handoff_waggle_app/SCREENS.md` §02 + `design-files/screens/chat.html`.
|
||||
|
||||
**Headline:** the conversation surface, activity stream (steps), tool cards, approval card, model
|
||||
pill, persona pill, composer, artifact card, and provenance primitive **all already exist** and are
|
||||
functional. The **right work-canvas (Variation B's ~42% live-drafting side panel) does NOT exist** —
|
||||
it must be built. There is also **no provenance pill inside the activity stream**, and the agent's
|
||||
streamed "thinking" steps render as a **flat inline list, not the design's collapsible Activity
|
||||
card with `⬡ provenance` pills**. The visual styling is current "Hive DS" (primary/secondary/muted,
|
||||
emerald/amber/sky/violet) — it needs the warm-token migration but the structure is mostly there.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current structure (with line refs)
|
||||
|
||||
### Component tree
|
||||
```
|
||||
ChatHost.tsx keep-alive portal host: 1 ChatWindowInstance per visited workspace
|
||||
└ ChatHostInstance portals into per-workspace container; composes title bar
|
||||
└ ChatWindowInstance.tsx data wiring: useChat + useSessions + model/team fetch
|
||||
└ ChatApp.tsx ALL of the chat UI (793 lines) — single big component
|
||||
├ <header> (chat-header) persona pill · Memory chip · storage/team chips · autonomy · model pill
|
||||
├ Agent Profile panel (collapsible)
|
||||
├ Pins bar
|
||||
├ <div scrollRef> thread messages.map → bubbles
|
||||
│ └ BlockRenderer (per assistant msg with blocks)
|
||||
│ ├ TextBlock / StepBlock / ToolUseBlock / ArtifactBlock / ModelSwitchBlock
|
||||
│ └ ToolCard (legacy tools[] path)
|
||||
│ └ FeedbackButtons · suggested-action chips
|
||||
│ └ ApprovalGate (pendingApproval)
|
||||
└ composer (textarea + slash menu + attach + send)
|
||||
```
|
||||
|
||||
### Key file:line anchors
|
||||
- **ChatHost.tsx** — keep-alive via React portals. `ChatSlot` (`:85`) is the seam node
|
||||
`WorkspaceRoute` passes into `WorkspaceDesktopApp`'s `chatSlot` prop. `ChatHostInstance` (`:96`)
|
||||
portals `<ChatWindowInstance>` into a stable per-workspace `<div>` (`getChatContainer` `:57`), kept
|
||||
alive (hidden, not unmounted) so in-flight SSE survives navigation. **The split canvas must live
|
||||
INSIDE this kept-alive subtree** (either in ChatApp or a wrapper it renders), or the canvas state
|
||||
is lost on navigation. ChatHost itself only does title + keep-alive; it is NOT the place to add a
|
||||
sibling canvas pane.
|
||||
- **ChatWindowInstance.tsx** — thin data layer. `FALLBACK_MODELS` (`:14`); calls `useChat` (`:98`)
|
||||
and `useSessions` (`:97`); fetches model list/current-model with a 20s retry loop (`:126–211`);
|
||||
`handleModelChange` (`:213`). Passes ~20 props straight into `ChatApp` (`:221`).
|
||||
- **ChatApp.tsx** — the entire rendered surface:
|
||||
- Header bar: `:746` (`data-testid="chat-header"`). Persona picker `:765`; **Memory chip** `:815`
|
||||
(always-visible trust signal, `Brain` icon); storage badge `:829`; team presence `:843`;
|
||||
overflow `⋯` menu in compact mode `:867`; **AutonomyToggle** `:920`; **model picker** `:931`.
|
||||
- `ToolCard` (`:99`) — legacy `msg.tools[]` render path (only used when a msg has no `blocks`).
|
||||
- `ApprovalGate` (`:223`) — the inline approval card with RiskBadge + Allow once / Always allow /
|
||||
Deny / Show details.
|
||||
- `AutonomyToggle` (`:339`) — Ask first / Trusted / Autopilot chip + TTL dropdown.
|
||||
- Thread render `:1050–1191`; per-message bubble `:1093`; `BlockRenderer` invocation `:1115`;
|
||||
plain-content fallback `:1121`; suggested-action chips `:1160`.
|
||||
- Composer `:1194–1236`; slash menu `:1195`; textarea `:1219` ("Message Waggle... (/ for commands)").
|
||||
- `WorkspaceBriefing` empty-state `:1051` (renders when `messages.length === 0`).
|
||||
- **chat-blocks/** (the progressive-disclosure renderers):
|
||||
- `BlockRenderer.tsx` — switch over block.type; routes completed file-writes to `ArtifactBlock`
|
||||
(`:34`), else `ToolUseBlock`.
|
||||
- `StepBlock.tsx` — a single agent "thinking" step: spinner/check + description. **Flat inline
|
||||
row — NOT wrapped in a collapsible Activity card and has NO provenance pill.**
|
||||
- `ToolUseBlock.tsx` — collapsible tool row (status icon + name + input summary + duration + raw
|
||||
JSON on expand).
|
||||
- `ArtifactBlock.tsx` — the Cowork "artifact card": icon + filename + "Created/Updated by the
|
||||
agent" + **Open in Files** (stashes deep-link + fires `waggle:open-app`). `isArtifactBlock` (`:21`).
|
||||
- `ModelSwitchBlock.tsx` — fallback-model banner.
|
||||
- **chat-header-layout.ts** — pure `shouldCollapseChatHeader(width)` decision (threshold 480px,
|
||||
`:16`); `CHAT_HEADER_OVERFLOW_CONTROLS` / `CHAT_HEADER_PRIMARY_CONTROLS` classify which chips fold.
|
||||
|
||||
### Container / layout the canvas must slot into
|
||||
`WorkspaceDesktopApp.tsx:841` — `{/* Body: main canvas + right context panel */}` is a
|
||||
`flex-1 flex overflow-hidden` row. The chat tab renders `chatSlot ?? <placeholder>` at `:882–902`
|
||||
inside `<main className="flex-1 min-w-0 overflow-auto">`. The chat slot is given the full main
|
||||
column. **The split canvas should be implemented as a horizontal flex INSIDE ChatApp's own root
|
||||
`<div className="flex h-full relative">` (`:697`)** — append the canvas `<aside>` as a sibling of
|
||||
the existing chat `<div className="flex flex-col flex-1 min-w-0">` (`:744`). That keeps it within
|
||||
the kept-alive portal subtree and reuses ChatApp's existing flex root.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data contract — how streamed turns / activity / artifacts arrive
|
||||
|
||||
### Source of truth: `useChat.ts` (apps/web/src/hooks/useChat.ts)
|
||||
- Returns `{ messages, isLoading, sendMessage, clearHistory, pendingApproval, approveAction }` (`:325`).
|
||||
- `sendMessage` (`:93`) appends a user `ChatMessage` then an empty assistant `ChatMessage`
|
||||
(`blocks: []`), then **iterates `adapter.sendMessage(...)` as an async generator of `StreamEvent`**
|
||||
(`:125`), reducing each event into the LAST assistant message's `blocks[]` immutably (`:130–262`).
|
||||
- **StreamEvent types** (`lib/types.ts:616`): `'token' | 'step' | 'tool_start' | 'tool_end' | 'done'
|
||||
| 'error' | 'approval_request' | 'approval_required' | 'model_switch' | 'notification'`.
|
||||
Adapter maps SSE event names to these in `adapter.ts:722–731` (`tool`→`tool_start`,
|
||||
`tool_result`→`tool_end`, etc.).
|
||||
- **Event → block reduction** (`useChat.ts`):
|
||||
- `token` (`:141`) → appends/extends the trailing `TextContentBlock`.
|
||||
- `step` (`:152`) → marks prior running steps done, pushes a new `StepContentBlock{status:'running'}`.
|
||||
**This is the "activity/thinking" stream — currently a flat sequence of StepBlocks, not grouped.**
|
||||
- `tool_start` (`:167`) → pushes a `ToolUseContentBlock{status:'running'}` AND mirrors into legacy
|
||||
`tools[]`.
|
||||
- `tool_end` (`:183`) → flips the matching tool block to `done` with `result`/`duration`.
|
||||
- `model_switch` (`:214`) → `ModelSwitchContentBlock`.
|
||||
- `error` (`:225`) → `ErrorContentBlock`.
|
||||
- `done` (`:231`) → marks all running blocks done; appends final text if none present.
|
||||
- `approval_request` / `approval_required` (`:248`) → `setPendingApproval(data)` — does NOT mutate
|
||||
blocks; surfaced as a single `pendingApproval` slot (one at a time).
|
||||
- `content` is kept in sync via `flattenBlocks` (`:45`) for copy/pins/search.
|
||||
- **History load**: `getHistory(workspaceId, sessionId)` → `ensureBlocks` backfills `blocks[]` for
|
||||
legacy messages (`:14`, `:83–91`).
|
||||
- **Adapter wire**: `adapter.sendMessage` (`adapter.ts:686`) POSTs `/api/chat` with
|
||||
`{workspaceId, message, sessionId, persona, autonomy, shape}` and parses SSE lines into StreamEvents.
|
||||
|
||||
### ContentBlock shapes (`lib/types.ts:461–503`)
|
||||
`TextContentBlock{type,blockId,content}` · `StepContentBlock{type,blockId,description,status}` ·
|
||||
`ToolUseContentBlock{type,id,name,input?,status,result?,duration?}` ·
|
||||
`ModelSwitchContentBlock{type,blockId,from,to,reason}` · `ErrorContentBlock{type,blockId,message}`.
|
||||
`ChatMessage{id,role,content,blocks?,timestamp,tools?,feedback?,pinned?,persona?}` (`:366`).
|
||||
`ApprovalRequest{requestId,toolName,description,input,riskLevel?,approvalClass?,trustSource?,
|
||||
explanation?,...}` (`:387`).
|
||||
|
||||
### Artifacts — how they arrive
|
||||
There is **no dedicated artifact/canvas stream channel.** An "artifact" today is derived purely from
|
||||
a completed `write_file`/`edit_file`/`file_write` tool block (`ArtifactBlock.isArtifactBlock` `:21`)
|
||||
and rendered as an inline card. The design's **live-drafting `teardown.md` canvas has no backing
|
||||
data source** — the streamed events carry no document body, only the tool's `input.path` + opaque
|
||||
`result` string. **Building the canvas means either** (a) deriving its content from the most recent
|
||||
file-write tool block's `input.content`/`result`, or (b) adding a new stream channel /
|
||||
artifact-fetch. This is the single biggest data gap (see §5).
|
||||
|
||||
### Provenance data
|
||||
The activity steps from the server are plain text descriptions — **the SSE `step` payload carries no
|
||||
structured `source`/`provenance` field.** The design's `⬡ mem://hive · provenance kept` pill has no
|
||||
backing data today; it would need either a richer `step` payload or a client-side heuristic. A reusable
|
||||
provenance UI primitive already exists: `components/ui/evidence-chip.tsx` (`EvidenceChip`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Test contract (must not break)
|
||||
|
||||
### `apps/web/src/test/chat-artifact-block.test.tsx`
|
||||
- `BlockRenderer` renders `chat-artifact-block` for a completed `write_file` with a path; shows the
|
||||
basename and "Created by the agent".
|
||||
- `edit_file` → "Updated by the agent".
|
||||
- running/error file-writes keep the generic tool row (NOT an artifact card).
|
||||
- `isArtifactBlock`: false for non-file tools / missing path; true for a done write_file.
|
||||
- **Open in Files** stashes the path deep-link and fires `waggle:open-app` with `appId:'files'`.
|
||||
- **Contract for PR3:** keep `data-testid="chat-artifact-block"` + `chat-artifact-open`, the
|
||||
Created/Updated copy, and the `isArtifactBlock` routing predicate intact. The canvas is ADDITIVE —
|
||||
the inline artifact card stays.
|
||||
|
||||
### `apps/web/src/test/p1a-chat-state.test.tsx`
|
||||
- `useChatWidgetState` persistence (persona, autonomy TTL + 10s auto-revert, P4 defaultAutonomy
|
||||
inheritance) under `waggle-chat-state-v1`.
|
||||
- `seedChat`/`takeChatSeed` one-shot semantics; `composeChatTitle` formatting.
|
||||
- **WorkspaceDesktopApp two-seam edit (§5.2):** seam (a) controlled `activeTab` + `onTabChange`
|
||||
(test IDs `ws-tab-chat`, `ws-tab-tasks`, `ws-tasks-tab`); **seam (b) the chat tab renders the
|
||||
provided `chatSlot` instead of the placeholder** (test IDs `chat-slot-stub`, and asserts
|
||||
`ws-chat-tab-open` is absent when a slot is given).
|
||||
- **Contract for PR3:** the `chatSlot` seam + `activeTab`/`onTabChange` props are load-bearing — the
|
||||
split canvas must NOT change `WorkspaceDesktopApp`'s public chatSlot contract; it lives inside the
|
||||
slot's subtree (ChatApp), not as a new prop on the desktop.
|
||||
|
||||
### Other related tests (grep before editing)
|
||||
`chat-blocks/TextBlock.test.tsx`; `context-rail-fetch.test.ts` (the `onContextRail` double-click path
|
||||
in ChatApp `:1095`); header-layout behavior is pinned via `chat-header-layout.ts` consumers.
|
||||
|
||||
---
|
||||
|
||||
## 4. Design spec — Variation B, with exact copy
|
||||
|
||||
### Context header (replaces current `chat-header` styling)
|
||||
- `wmark` hex "C" (honey gradient) + workspace name **"Competitive Intelligence"** + mono sub
|
||||
**"workspace · 142 memories · 9 sources"**.
|
||||
- **Model pill** (right): pill, `--surface` bg, `--line` border, healthy live dot, title
|
||||
"Waggle picked the model — click to override", text **"Model: auto · Claude Sonnet"** (`b` on
|
||||
"auto"). → maps to current model picker (`ChatApp.tsx:931`) but restyled as a pill with a live dot
|
||||
and the "auto ·" prefix.
|
||||
- **Memory icon button** (right) — `iconbtn`, brain glyph, title "Memory & context". → current Memory
|
||||
chip (`:815`) becomes this icon button.
|
||||
|
||||
### Thread (max-width 760px; **narrows to ~620px when canvas open**)
|
||||
- **User message:** `--surface` bubble, **asymmetric radius `4px 14px 14px 14px`**, 15.5px/1.55.
|
||||
(Current user bubble is `bg-primary rounded-xl` `:1108` — needs warm surface + asymmetric radius.)
|
||||
- **Bot message:** hex "W" avatar (honey gradient); meta line **"Waggle · Analyst · Claude Sonnet"**
|
||||
(`who` `b` on "Waggle"). Prose 15.5px/1.62; bullet lists use honey `◆` markers.
|
||||
- **Activity stream card (the "magic"):** collapsible, `--bg-2` bg, `--line-soft` border, radius
|
||||
`--r`. Header: violet **spark** icon + **"Worked across memory, web & files · 6 steps · 38s"**
|
||||
+ chevron (rotates 90° when open). **Default-open on the active turn.** Each step row (`.astep`):
|
||||
colored dot + text + optional **provenance pill** — mono, `--intel` color, `--intel-wash` bg,
|
||||
format **`⬡ mem://hive · provenance kept`**. Exact step copy from chat.html:
|
||||
- (intel dot) "Recalled **6 memories** from your hive — last quarter's landscape, the Mem0
|
||||
teardown, your pricing notes." → prov `⬡ mem://hive · provenance kept`
|
||||
- (web/cyan dot) "Searched **9 competitor sites** for pricing & positioning `(mem0, letta,
|
||||
langmem, notion…)`"
|
||||
- (work/blue dot) "Compared against your **last teardown** — flagged 3 changes since March."
|
||||
- (healthy dot) "Drafted **teardown.md** — exec summary, landscape table, opening."
|
||||
- **Bot prose after activity:** "Done. Here's the shape of it — full draft is in **teardown.md**."
|
||||
+ 3 honey-bullet items + "Want me to turn the opening into a one-page brief for the board, or
|
||||
export the table to the pricing sheet?"
|
||||
- **Approval card** (`--honey-wash` bg, `--attention` border, warning icon):
|
||||
- Title: **"Approve before I leave your machine"**
|
||||
- Body: "This step writes to an external system — `Salesforce › Q2 Pricing` (mono). Everything
|
||||
else stayed local. I'll export the 9-row table and nothing else."
|
||||
- Actions: **"Approve & export"** (honey) / **"Not now"** (ghost).
|
||||
- → maps to current `ApprovalGate` (`:223`) — restyle to honey-wash; keep RiskBadge + the
|
||||
Always-allow gating logic.
|
||||
|
||||
### Composer
|
||||
- Rounded box, `--surface`, `:focus-within` → `--honey-line` + `--honey-glow`. Textarea placeholder
|
||||
**"Reply, or ask Waggle to take the next step…"**. Tool chips row: **Attach** / **Persona: Analyst**
|
||||
/ **Tools**; spacer; mono hint **"⏎ send · ⌘K commands"**; honey send button (`↑`, 40px).
|
||||
→ current composer (`:1194`) restyle; placeholder + hint copy change; persona/tools become chips.
|
||||
|
||||
### Variation B — right work canvas (~42% width)
|
||||
- `<aside class="canvas">` `--bg-2`, `--line-soft` left border, `width:42%` (transition .25s).
|
||||
- Canvas head: title **"teardown.md"** + healthy mono tag **"● live draft"** + an "Open in
|
||||
Artifacts" icon button (external-link glyph).
|
||||
- Canvas body = `.doc`: H1 **"Q2 Competitive Teardown"**; mono docsub **"draft · 9 competitors ·
|
||||
updated just now by Waggle"**; honey mono H2 section labels ("Executive summary", "Landscape");
|
||||
a landscape `<table>` (Player / Memory / Local-first: Mem0 Cloud No · Letta Agent-centric Partial
|
||||
· LangMem Toy-tier No · Notion AI Doc-scoped No); a trailing paragraph with a **blinking honey
|
||||
type-cursor** (`▍`, `@keyframes bl` 1s steps(2)).
|
||||
- Responsive: `@media (max-width:820px)` hides the canvas (chat goes full width).
|
||||
|
||||
### Interactions
|
||||
- Clicking an activity header toggles its steps (current StepBlocks have no grouping/toggle — new).
|
||||
- Canvas type-cursor blinks on the live draft.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gap table (current → design)
|
||||
|
||||
| # | Area | Current | Design (Variation B) | Gap / action |
|
||||
|---|------|---------|----------------------|--------------|
|
||||
| 1 | **Right work canvas** | **Does not exist** anywhere (`grep canvas` → only WorkspaceDesktopApp layout comments + an unrelated KG viz). Chat is single-pane. | ~42% live-drafting `teardown.md` aside with header, doc body, blinking honey cursor; thread narrows to 620px when open. | **BUILD NEW.** New `<ChatWorkCanvas>` aside as sibling of ChatApp's chat column (`:744`). Needs open/close state + a data source for the doc body (none exists — see #2). |
|
||||
| 2 | **Canvas content source** | No artifact body in the stream — only tool `input.path` + opaque `result`. | Live document with sections/table that updates as the agent drafts. | **DATA GAP.** Derive from latest file-write tool block's `input.content`/`result`, OR add a stream channel / artifact fetch. Simplest PR3: show the last completed artifact's rendered content; "live drafting" cursor is cosmetic. |
|
||||
| 3 | **Activity stream grouping** | Flat `StepBlock` rows inline (`StepBlock.tsx`), no card, no toggle, no header summary. | Collapsible Activity card with violet spark + "Worked across … · N steps · Ns" header + chevron; default-open on active turn. | **BUILD.** New `ActivityCard` that groups consecutive `step` (and tool) blocks; BlockRenderer must collapse a run of steps into one card. Derive "N steps · Ns" from the grouped blocks. |
|
||||
| 4 | **Provenance pill in steps** | None (SSE `step` carries no source field). `EvidenceChip` primitive exists but unused in chat. | `⬡ mem://hive · provenance kept` mono `--intel` pill per step. | **DATA + UI GAP.** No backing data. PR3 can render the pill only when a step/tool exposes a source; otherwise omit (don't fabricate). Reuse `EvidenceChip` styled to `--intel`/`--intel-wash`. |
|
||||
| 5 | **Model pill** | Picker button: `Cpu` icon + raw model string + chevron (`:931`). | Pill w/ healthy live dot + "Model: auto · Claude Sonnet"; honey-line hover. | **RESTYLE.** No "auto" sentinel surfaced today; show "auto ·" when model is the default/unset, plus a live dot. |
|
||||
| 6 | **User bubble radius/color** | `bg-primary text-primary-foreground rounded-xl` (`:1108`). | `--surface` bubble, asymmetric `4px 14px 14px 14px`. | **RESTYLE** to warm surface + asymmetric radius. |
|
||||
| 7 | **Bot meta line** | Avatar + Sparkles prefix; no "Waggle · Analyst · Claude Sonnet" meta row. | hex W avatar + meta "Waggle · Analyst · Claude Sonnet". | **ADD** meta row above bot prose (persona name + model). |
|
||||
| 8 | **Approval card** | `ApprovalGate` honey/amber-ish, RiskBadge, Allow once/Always allow/Deny/Show details (`:223`). | honey-wash card, "Approve before I leave your machine", "Approve & export"/"Not now". | **RESTYLE + copy.** Keep RiskBadge + Always-allow gating; warm tokens; external-write framing copy. |
|
||||
| 9 | **Composer copy + chips** | placeholder "Message Waggle... (/ for commands)"; Paperclip + Send only; no persona/tools chips, no "⏎ send · ⌘K" hint. | placeholder "Reply, or ask Waggle…"; Attach/Persona/Tools chips + mono "⏎ send · ⌘K commands" hint; honey ↑ send. | **RESTYLE + ADD** chips row + hint; new copy. |
|
||||
| 10 | **Header sub / context** | persona pill + Memory chip + storage/team chips + autonomy + model (no "142 memories · 9 sources" sub, no workspace hex). | hex avatar + name + mono "workspace · 142 memories · 9 sources" + model pill + memory icon. | **RESTYLE.** Add hex avatar + memory/sources sub (data from workspace context). Autonomy/storage/team chips not in design header — relocate or drop into ⌘K/overflow. |
|
||||
| 11 | **Warm tokens** | Hive DS classes (`bg-primary`, `text-emerald-400`, `bg-secondary`, `border-border`, etc.). | Warm graphite/paper tokens (`--surface`, `--honey`, `--intel`, `--bg-2`, `--line-soft`). | **MIGRATE** color classes to the PR1 warm tokens (already in `index.css`). |
|
||||
| 12 | **Activity default-open on active turn** | StepBlocks always visible (no collapse). | Activity card default-open on the active turn, collapsed on prior turns. | **BEHAVIOR.** Track which turn is active; collapse historical activity cards. |
|
||||
|
||||
**Not gaps (already correct / reusable):** SSE streaming + block reduction (`useChat`), tool cards,
|
||||
artifact card + Open-in-Files, feedback buttons, slash menu, pins, persona picker, autonomy toggle,
|
||||
keep-alive across navigation, the `chatSlot` seam, `EvidenceChip` provenance primitive, warm tokens
|
||||
in `index.css`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Reuse + build
|
||||
|
||||
### Reuse as-is
|
||||
- **`useChat.ts`** — the entire stream/reduction contract is design-compatible. The Activity card +
|
||||
canvas are pure RENDER concerns over the existing `blocks[]`; no hook change required for a basic
|
||||
build (canvas content derivation may want a small selector helper).
|
||||
- **`ChatHost.tsx` keep-alive + `ChatSlot` seam** — unchanged. Canvas state lives inside the
|
||||
kept-alive ChatApp subtree, so it survives navigation for free.
|
||||
- **`ArtifactBlock.tsx`** — keep the inline card AND reuse its `iconFor`/`isArtifactBlock`/path logic
|
||||
to feed the canvas (open the latest artifact in the canvas instead of/in addition to "Open in Files").
|
||||
- **`EvidenceChip`** (`components/ui/evidence-chip.tsx`) — base for the `⬡ provenance` pill (restyle
|
||||
to `--intel`).
|
||||
- **`ApprovalGate`** + `RiskBadge` + `canAlwaysAllow` — keep logic, restyle to honey-wash.
|
||||
- **`AutonomyToggle`**, slash menu, pins, `FeedbackButtons`, `WorkspaceBriefing` — keep.
|
||||
- **`chat-header-layout.ts`** — keep the overflow decision (chips not in the design header fold here).
|
||||
|
||||
### Build new
|
||||
- **`ChatWorkCanvas` aside** — append as a sibling of ChatApp's chat column inside ChatApp root
|
||||
(`ChatApp.tsx:697` flex root; chat column `:744`). Props: `open`, `artifact` (path + body), `onClose`.
|
||||
Width 42%; thread column `max-width` drops to ~620px when open. Honor `@media (max-width:820px)` →
|
||||
hide canvas. This keeps it within the keep-alive portal subtree (do NOT add it as a `WorkspaceDesktopApp`
|
||||
pane — that breaks the `chatSlot` test contract and lives outside keep-alive).
|
||||
- **`ActivityCard`** — a grouping wrapper in `chat-blocks`: BlockRenderer collapses a consecutive run
|
||||
of `step` (+ optionally tool) blocks into one collapsible card with the "Worked across … · N steps
|
||||
· Ns" header (derive count from grouped steps; duration from summed/last tool durations if present),
|
||||
violet spark icon, chevron toggle, default-open when `isStreaming`/active turn.
|
||||
- **Canvas content selector** — derive the canvas doc from the most recent completed
|
||||
`write_file`/`edit_file` block (path + `input.content`/`result`); render markdown via the app's
|
||||
existing markdown path (TextBlock already renders prose — reuse its renderer for the doc body).
|
||||
The blinking type-cursor is cosmetic, shown while `isStreaming`.
|
||||
- **Provenance data** — render the step provenance pill ONLY when the data exists; do not fabricate.
|
||||
Flag to backend owners that the SSE `step` payload needs a structured `source` field to fully match
|
||||
the design (`docs/redesign-warm-hive/pr3-recon` follow-up).
|
||||
|
||||
### Is there already an artifact-canvas / side panel? — NO.
|
||||
Confirmed by grep across `apps/web/src` for `canvas` / `work canvas` / `artifact-canvas`: the only
|
||||
matches are `WorkspaceDesktopApp.tsx` layout comments (an unrelated "right context panel" on the
|
||||
Overview tab, NOT a chat canvas) and a knowledge-graph canvas in Memory. The chat surface
|
||||
(`ChatApp.tsx`) is strictly single-pane (`flex h-full` with one `flex-col flex-1` column, `:697`/`:744`).
|
||||
**The split work canvas must be built from scratch.**
|
||||
|
||||
### Risk notes
|
||||
- Canvas MUST live inside the kept-alive subtree (ChatApp), else its state resets on every nav
|
||||
(`ChatHost.tsx` keep-alive only covers the portal subtree).
|
||||
- Do not change `WorkspaceDesktopApp`'s `chatSlot`/`activeTab` props — `p1a-chat-state.test.tsx`
|
||||
seam (a)/(b) pins them.
|
||||
- Keep `chat-artifact-block` / `chat-artifact-open` test IDs + Created/Updated copy + `isArtifactBlock`
|
||||
predicate — `chat-artifact-block.test.tsx` pins them; the canvas is additive.
|
||||
293
docs/redesign-warm-hive/pr3-recon/home.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# PR3 Recon — SCREEN 01 · Home / Cockpit (ship Variation A "Editorial")
|
||||
|
||||
Maps the **current** Home/Cockpit implementation against the warm-Hive design spec for
|
||||
SCREEN 01. Scope: what renders today, the live data contract, the test contract that
|
||||
must keep passing, the Editorial target with exact copy, the gap list, and the
|
||||
reuse/build plan.
|
||||
|
||||
Primary files:
|
||||
- Current UI: `apps/web/src/components/os/apps/HomeCockpit.tsx` (the real Home; **this is the one PR3 rebuilds**)
|
||||
- Route wrapper: `apps/web/src/routes/HomeRoute.tsx`
|
||||
- `CockpitApp.tsx` (`apps/web/src/components/os/apps/CockpitApp.tsx`) is a **separate** system-health dashboard (Mission-Control-style), **NOT** the Home surface — see note in §1.
|
||||
- Server contract: `packages/server/src/local/routes/home.ts`
|
||||
- FE types: `apps/web/src/lib/types.ts:266-331`
|
||||
- Test contract: `apps/web/src/test/p2-home-desktop.test.tsx`
|
||||
- Design: `docs/design_handoff_waggle_app/SCREENS.md` §"01 · Home / Cockpit" + `design-files/screens/home.html` (view `#view-a`)
|
||||
|
||||
---
|
||||
|
||||
## 1. Current structure — what `HomeCockpit.tsx` renders today (section by section)
|
||||
|
||||
`HomeCockpit` is the default `/home` surface (`HomeRoute.tsx:13`). It is a single
|
||||
scrolling column, `p-6 max-w-3xl mx-auto` on the sub-states and `max-w-4xl mx-auto` on
|
||||
the normal render (`HomeCockpit.tsx:580`). Data loads in `load()`
|
||||
(`HomeCockpit.tsx:467-497`): `adapter.getHomeBriefing()` then a best-effort
|
||||
`adapter.getHomeOvernight()`; deferred until `useService().connecting` settles
|
||||
(cold-load 401 race guard, `:499-507`).
|
||||
|
||||
**Render states (root, `:509-650`):**
|
||||
1. **Loading** — `CockpitSkeleton` (`:84-98`), `data-testid="home-cockpit-loading"`.
|
||||
2. **Permission denied (403)** — `:514-527`, `data-testid="home-cockpit-permission-denied"`.
|
||||
3. **Load error / offline** — `:531-554`, `data-testid="home-cockpit-error"` + Retry.
|
||||
4. **First-run empty** — `FirstRunEmpty` (`:101-125`), `data-testid="home-cockpit-empty"`, "Create your first workspace" CTA.
|
||||
5. **Normal** — `:579-649`, `data-testid="home-cockpit"`.
|
||||
|
||||
**Normal render, top to bottom:**
|
||||
- **`GreetingHeader`** (`:136-159`, rendered `:581`): H1 = `briefing.greeting` (`text-2xl font-display font-bold`), sub = `formatBriefingDate(briefing.date)` rendered as a human date (`:130-134`). Right side: optional offline "Local only" pill + a static `Ctrl+K` chip (`:153-156`). **No mono date row, no live dot, no streak chip.**
|
||||
- **Attention banner** (`:583-599`, `data-testid="home-cockpit-attention-banner"`): only when `overnight.failures.length > 0`. Honey-tinted `role="alert"`.
|
||||
- **J08 review banner** (`:601-625`, `data-testid="home-cockpit-review-banner"`): only when `briefing.needsReviewCount > 0`; "N imported memories need your review" + Review CTA that dispatches `waggle:open-app {appId:'memory', filter:'unreviewed'}` (`:573-577`).
|
||||
- **`RecentWorkspacesPanel`** (`:162-227`, rendered `:627-632`): section head **"You were working on"** (`:174-176`), 2-col grid (`sm:grid-cols-2`). Each card: name + `group` chip, optional 2-line `summary`, `lastActive` relative time + `pendingCount` "pending" warning, a **"Continue →"** button (`:208-215`), and a `WorkspaceActionsMenu` kebab (G1 rename/archive/delete). Card body click → `onOpenDesktop`. Returns null when 0 cards.
|
||||
- **`OvernightPanel`** (`:230-288`, rendered `:637`, suppressed when offline): section head **"Overnight"**, a 3-counter grid (Memories consolidated / Artifacts created / Automations completed), then a failures block (deep-links each failure to the Automation Center logs, `:271-280`). Hidden when no activity.
|
||||
- **`SuggestedActionsPanel`** (`:321-343`, rendered `:639`): section head **"Suggested next actions"**, wrap of pill buttons from `briefing.suggestedActions`, each → `onContinue(a.workspaceId, a.sessionId)`.
|
||||
- **`UpNextPanel`** (`:291-318`, rendered `:643`): section head **"Up next"**, list of up to 6 items (event/task/schedule icons). Returns null when 0 items.
|
||||
- **`QuickCapturePanel`** (`:346-434`, rendered `:645`): "Quick capture" — a 4-kind segmented selector (note/task/link/file) + text input + Capture button → `adapter.quickCapture()`. **Not in the Editorial design.**
|
||||
- **`ActiveModelsTile`** (`:437-448`, rendered `:647`): tiny "Active models: …" chips row when `briefing.activeModels` present.
|
||||
|
||||
**Styling today:** uses shadcn semantic classes (`bg-secondary/30`, `border-border/30`,
|
||||
`text-muted-foreground`) and inline `var(--sem-intelligence)` / `--sem-attention` /
|
||||
`--sem-work` / `--sem-healthy` / `--sem-risk` semantic tokens. Layout is **dense and
|
||||
utilitarian** (10-13px text, `rounded-xl`, compact panels) — the opposite of the
|
||||
Editorial spec's calm, large-type, story-led 920px column.
|
||||
|
||||
> **Note on `CockpitApp.tsx`:** despite the name, this is the system-health/ops
|
||||
> dashboard (System Health, Cost, Memory Weaver, Cron, Connectors, Audit Trail,
|
||||
> ComplianceDashboard) on a 30s refresh. It is **not** the Home surface and **not in
|
||||
> scope for SCREEN 01** — it corresponds to "Mission Control" (SCREENS §16) /
|
||||
> "surfaces" (§08). Listed in the task only to disambiguate; PR3 Home work happens
|
||||
> entirely in `HomeCockpit.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data contract — `HomeBriefing` shape + feeding routes (real vs mocked)
|
||||
|
||||
### Types (FE mirror `apps/web/src/lib/types.ts:266-331`; server `home.ts:43-95`)
|
||||
|
||||
```ts
|
||||
interface HomeBriefing {
|
||||
greeting: string; // server builds via buildTimeAwareGreeting + personalizeGreeting (home.ts:401-407)
|
||||
userName?: string; // from IdentityLayer on personal mind (home.ts:261-270); optional
|
||||
date: string; // now.toISOString() (home.ts:412) — RAW ISO, FE must humanize
|
||||
recentWorkspaces: RecentWorkspaceCard[];
|
||||
suggestedActions: SuggestedAction[];
|
||||
upNext: UpNextItem[];
|
||||
activeModels?: string[]; // OPTIONAL — server briefing NEVER populates it (no producer in home.ts)
|
||||
isFirstRun: boolean; // ranked.length === 0 (home.ts:418)
|
||||
needsReviewCount?: number; // J08 unreviewed personal-mind frames (home.ts:382-389)
|
||||
}
|
||||
|
||||
interface RecentWorkspaceCard {
|
||||
id; name; group; // group = workspace.group string
|
||||
summary?; // state.recentDecisions[0].content (home.ts:314-316)
|
||||
lastActive: string; // ISO; ranking timestamp
|
||||
pendingCount: number; // state.pending.length + state.blocked.length
|
||||
continueSessionId?; // NOTE: server never sets it (home.ts card builder omits) → Continue has no session seed
|
||||
}
|
||||
|
||||
interface SuggestedAction { label; workspaceId; sessionId?; kind; } // kind always 'next-action' from server
|
||||
interface UpNextItem { id; label; workspaceId?; at?; kind: 'event'|'task'|'schedule'; }
|
||||
// server only ever emits kind:'schedule' from cron; 'event'/'task' are type-only
|
||||
// server NEVER sets `at` → UpNextPanel's time column is always empty today
|
||||
|
||||
interface OvernightSummary { // GET /api/home/overnight (separate call)
|
||||
consolidated; artifactsCreated; automationsCompleted;
|
||||
failures: OvernightFailure[]; // {id,label,automationId?,error,at} from cron execution history
|
||||
window?: { from; to };
|
||||
}
|
||||
```
|
||||
|
||||
### Routes feeding Home
|
||||
| Route | Method | Adapter | Source / realness |
|
||||
|---|---|---|---|
|
||||
| `/api/home/briefing` | GET | `adapter.getHomeBriefing()` (`adapter.ts:612-615`) | **Real.** Server-side cross-workspace fan-out (`home.ts:257-423`); personal-only (A2), excludes team/archived. Reuses `buildWorkspaceState`, `buildTimeAwareGreeting`, `buildUpcomingSchedules`, `IdentityLayer`, cron store, audit DB. |
|
||||
| `/api/home/overnight` | GET | `adapter.getHomeOvernight(since?)` (`adapter.ts:617-621`) | **Real.** Audit-event counts (`memory_write` → consolidated; file-write `tool_call` → artifacts) + cron execution history → automations/failures (`home.ts:425-503`). |
|
||||
| `/api/quick-capture` | POST | `adapter.quickCapture()` (`adapter.ts:623-627`) | **Real.** Used by `QuickCapturePanel` (not in Editorial layout). |
|
||||
|
||||
### Real vs mocked / missing, per design need
|
||||
- **REAL:** greeting, userName, date (ISO), recentWorkspaces (name/group/summary/lastActive/pendingCount), suggestedActions, upNext (schedule labels only), overnight counters + failures, needsReviewCount.
|
||||
- **NOT produced by the server (treat as absent):**
|
||||
- `continueSessionId` — never set → "Continue" can't target a session (already a known gap, `HomeRoute.tsx:16-19`).
|
||||
- `UpNextItem.at` (time) and `event`/`task` kinds — never set; only `schedule` from cron.
|
||||
- `activeModels` — typed-optional, **no producer**; tile never shows from the real route.
|
||||
- **NO DATA ANYWHERE (must mock for the Editorial design):**
|
||||
- **🔥 streak chip** — `grep` across `packages/server/src` and `apps/web/src` finds **no** `streak` field. Habit-loop spec (SCREENS §15) says the streak lives on Home but it is unimplemented. **Needs-mock** (or a follow-up backend field).
|
||||
- **Overnight "story" sentence** + **run chips with labels** ("Teardown drafted · 9 competitors") — server gives raw counts, not a composed narrative or per-run labels. The narrative must be **composed client-side** from `OvernightSummary` (counts + failures), and the first "Teardown drafted" run chip has **no backing field** (would need the top suggestedAction/workspace summary as a proxy) → **partial mock**.
|
||||
- **Workspace card hex avatar glyph / "agent live" badge** — no `glyph`/`live` field; derive glyph from `name[0]`, and there is no per-card live-agent flag → **derive / mock badge**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Test contract — `apps/web/src/test/p2-home-desktop.test.tsx`
|
||||
|
||||
These assertions constrain the rebuild (the `describe('HomeCockpit (P2)')` block,
|
||||
`:73-129`). All must keep passing. The harness renders `HomeCockpit` bare with mocked
|
||||
`adapter`, `useService` (`{connecting:false, connected:true}`), offline=false, and a
|
||||
stub `ShellContext` (`:38-46`).
|
||||
|
||||
Required behaviors / DOM contract:
|
||||
1. **Root testid** — after load, `screen.getByTestId('home-cockpit')` must exist (`:81`). **Keep `data-testid="home-cockpit"` on the normal-render root.**
|
||||
2. **J08 review banner** (`:84-98`) — with `needsReviewCount:3`, `data-testid="home-cockpit-review-banner"` renders text containing **"3 imported memories need your review"**, and `data-testid="home-cockpit-review-cta"` click dispatches exactly `waggle:open-app` with detail `{ appId:'memory', filter:'unreviewed' }`. **Keep the banner, its copy pattern, the CTA testid, and the event payload.**
|
||||
3. **Banner omitted at 0/undefined** (`:100-103`) — no `home-cockpit-review-banner` when `needsReviewCount` is undefined.
|
||||
4. **Up next omitted when empty** (`:105-111`) — no `home-cockpit-upnext` when `upNext` is `[]` or undefined.
|
||||
5. **Up next present with items** (`:113-118`) — `home-cockpit-upnext` contains the item label ("Weekly digest") when ≥1 item.
|
||||
6. **Human date** (`:120-128`) — the raw ISO (`2026-06-11T07:42:13.512Z`) must **not** appear; the date must render via `new Date(iso).toLocaleDateString(undefined, {weekday:'long', month:'long', day:'numeric'})`. **Keep `formatBriefingDate` semantics; the mono date row must humanize, not print ISO.**
|
||||
|
||||
The mocked briefing factory (`:57-67`) defines the minimum shape the component must
|
||||
tolerate: `{greeting, userName:'Marko', date, recentWorkspaces:[], suggestedActions:[],
|
||||
upNext:[], isFirstRun:false, needsReviewCount:0}`. The Editorial rebuild must still
|
||||
render `home-cockpit` with all-empty arrays (no crash on empty overnight/workspaces).
|
||||
|
||||
**Implication:** PR3 may freely restyle and re-lay-out, but must preserve these
|
||||
testids + behaviors: `home-cockpit`, `home-cockpit-review-banner`,
|
||||
`home-cockpit-review-cta` (+ event payload), `home-cockpit-upnext` (present/absent
|
||||
rules), and human-date rendering. The `up next` empty/present rule means the Editorial
|
||||
"Up next" (if kept) stays conditional. Other testids (`home-cockpit-overnight`,
|
||||
`home-cockpit-suggested`, `home-cockpit-continue-*`, quickcapture testids) are **not**
|
||||
asserted in this file — they can be renamed/removed if their features are reshaped.
|
||||
|
||||
---
|
||||
|
||||
## 4. Design spec (Editorial / Variation A) — target sections + exact copy
|
||||
|
||||
From `SCREENS.md` §01 (ship = Variation A) and `home.html` `#view-a` (`:157-186`).
|
||||
**Layout:** single centered column, **max-width 920px, 46px top padding**
|
||||
(`home.html:31`). Sections top to bottom:
|
||||
|
||||
### 4.1 Greeting (`home.html:158-161`)
|
||||
- **Mono date row** (`--honey`, uppercase, `.1em` tracking) with a **live dot**
|
||||
(`.dot-live`, `--healthy`) on the left, and a **right-aligned 🔥 streak chip**
|
||||
(pill, `--honey-wash` bg, `--honey-line` border, `--honey` text).
|
||||
- **H1** Hanken 600, `clamp(34px,5vw,52px)`, line-height 1.02; the keyword **"ahead"**
|
||||
is honey (`em`, not italic; `--honey`).
|
||||
- **Exact copy:** date `"Friday · June 14 · 8:42"` (compose from `briefing.date`);
|
||||
streak `"🔥 12-day streak"`; H1 = **"Good morning, Mara."** / **"You're _ahead_ of yesterday."**
|
||||
- Maps to: greeting → `briefing.greeting` (real, already personalized server-side; the design's literal "Good morning, Mara." is `buildTimeAwareGreeting`'s output). "You're ahead of yesterday" second line + the honey "ahead" → **needs-mock / composed** (no "ahead vs yesterday" signal exists). Date → **real** (`briefing.date`, reformatted with time). Streak → **needs-mock**.
|
||||
|
||||
### 4.2 Overnight hero card (`home.html:163-172`)
|
||||
- Radius `--r-xl`, gradient `--surface → --surface-2`, `--shadow`, soft honey radial
|
||||
glow top-right (`::after`). Mono eyebrow with `--intel` dot.
|
||||
- **Eyebrow (exact):** "While you slept".
|
||||
- **Story line (exact):** Hanken 600, clamp 21→28px, max-width 30ch; honey key numbers (`b`):
|
||||
**"Waggle finished the _Q2 competitor teardown_, folded _14 new memories_ into the hive, and ran into _one snag_ worth a look."**
|
||||
- **Run chips (exact, status dot + label):**
|
||||
- `"Teardown drafted · 9 competitors"` (dot `--healthy`)
|
||||
- `"14 memories consolidated"` (dot `--intel`)
|
||||
- `"2 artifacts created"` (dot `--work`)
|
||||
- `"1 export failed"` (dot `--risk`)
|
||||
- Maps to: counters are **real** (`overnight.consolidated`, `.artifactsCreated`, `failures.length`). The **composed sentence** + the **"Teardown drafted · 9 competitors"** label are **needs-mock/composed** (no narrative producer; no per-run "9 competitors" field). Build the sentence from counts + failures client-side; degrade gracefully when overnight is null/empty (must still render `home-cockpit`).
|
||||
|
||||
### 4.3 "Pick up where you left off" (`home.html:174-175, 287-299`)
|
||||
- Section head = mono uppercase `--text-dim` with trailing hairline rule (`.sec-h`).
|
||||
- 2-col grid (`.ws-grid`) of `.ws-card`: **hex avatar** (honey gradient, glyph =
|
||||
first letter), **title** (16px, 650), **time** (mono `--text-dim`, e.g. "2h ago"),
|
||||
**summary** (13.5px `--text-muted`), footer = **"Continue →"** (honey) + optional
|
||||
**status badge** (`pend` honey-wash "3 to review" / `live` healthy-wash "agent live").
|
||||
Hover: honey border + `translateY(-3px)` + `--shadow`.
|
||||
- Maps to: **real** — `recentWorkspaces[].name`, `.summary`, `formatRelative(lastActive)`, `.pendingCount` (→ "N to review"/"N pending"). Hex glyph = derive from `name[0]`. "agent live" badge → **no field, mock/omit**. **Section head copy must change** "You were working on" → **"Pick up where you left off"**.
|
||||
|
||||
### 4.4 "Waggle suggests" (`home.html:177-178, 301-306`)
|
||||
- Section head "Waggle suggests". Stacked **`.move` rows**: tinted icon tile
|
||||
(`--*-wash` bg), **title** (14.5px 600) + **sub** (12.5px `--text-muted`), arrow
|
||||
that slides on hover.
|
||||
- **Exact sample copy (design data):** "Review the competitor teardown" / "9
|
||||
competitors · ready since 02:40 · ~6 min read"; "Send the board update" / "Draft
|
||||
built from this week's work in Q2 Board Deck"; "Confirm 3 imported memories" / "From
|
||||
Tuesday's pricing call — Waggle wants your sign-off".
|
||||
- Maps to: title → **real** `suggestedActions[].label`. The **sub-line** has no
|
||||
backing field (server emits label only, `kind:'next-action'`) → **needs-mock/derive**
|
||||
(e.g. workspace name + relative time). Section head copy "Suggested next actions" →
|
||||
**"Waggle suggests"**. Rows become **stacked** (not pills).
|
||||
|
||||
### 4.5 Ask bar (`home.html:180-185`)
|
||||
- Full-width pill (`.ask`), honey **"+"** icon left, text input
|
||||
(placeholder **"Start something new — "draft the board update from this week's
|
||||
work"…""**), mono **"⌘K"** hint, honey round **send** button ("→"). Focus → honey
|
||||
border + glow.
|
||||
- Maps to: **net-new on Home.** No current "ask bar" on HomeCockpit. Wire send →
|
||||
`onContinue`/new-chat or open ⌘K (PR2 `CommandCenter`). This **replaces** the
|
||||
current QuickCapturePanel as the primary input affordance. The "+" can keep a
|
||||
quick-capture role, but the design's primary intent is "start a task".
|
||||
|
||||
### 4.6 Not in Editorial (drop or relocate)
|
||||
- **Quick capture segmented panel** — replaced by the ask bar; the
|
||||
`adapter.quickCapture` API can be kept behind the "+" icon or dropped from Home.
|
||||
- **Active models tile** — not in Editorial (and route never populates it). Drop.
|
||||
- **Attention/review banners** — not literally in the Editorial mock, but the **J08
|
||||
review banner is test-locked** (keep it; can be styled as a run-chip-adjacent
|
||||
attention row or kept above the hero). The overnight-failure attention banner maps
|
||||
naturally onto the "1 export failed" run chip + (optionally) Variation C's risk
|
||||
alert pattern.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gap list
|
||||
|
||||
| Design section | Current state | Gap | Data available? | Severity |
|
||||
|---|---|---|---|---|
|
||||
| 920px centered column, 46px top pad | `max-w-4xl mx-auto p-6` dense | Re-layout to 920px / `pt-[46px]`, larger type scale | n/a (layout) | MED |
|
||||
| Mono date row + live dot | Plain `text-sm` sub under H1, no dot | Add mono uppercase honey date row + `.dot-live` | date = real; format with weekday+time | MED |
|
||||
| 🔥 streak chip (right pill) | Absent | Add streak chip | **No data** — needs-mock or new backend field | HIGH |
|
||||
| H1 with honey "ahead" keyword | H1 = raw greeting, no honey span | Two-line H1; honey-span a keyword | greeting real; "ahead of yesterday" composed | MED |
|
||||
| Overnight **hero** card (gradient + glow) | `OvernightPanel` = 3 plain counters + failures, suppressed offline, hidden when empty | Rebuild as hero card w/ eyebrow + story + run chips; must still render when empty/null | counts real; **story + run-chip labels composed/mock** | HIGH |
|
||||
| Run chips (4, status-dotted) | Counter tiles only | Render chips w/ status dots; map counts | 3 of 4 real (consolidated/artifacts/failed); "Teardown drafted · 9 competitors" mock | MED |
|
||||
| "Pick up where you left off" head | "You were working on" | Copy + `.sec-h` hairline style | n/a (copy) | LOW |
|
||||
| Workspace cards: hex avatar + 16px title + Continue→ + badge | Cards w/ name+group chip, Continue button, kebab | Add hex avatar (glyph from name), restyle; keep Continue + kebab | name/summary/time/pending real; glyph derived; "agent live" mock/omit | MED |
|
||||
| "Waggle suggests" stacked rows w/ sub-line | "Suggested next actions" pills, label-only | Copy + restyle to `.move` rows; add sub-line | label real; **sub-line composed/mock** | MED |
|
||||
| Ask bar (pill, +/⌘K/send) | None on Home (QuickCapture panel instead) | Net-new ask bar; wire to chat/⌘K | n/a (action wiring) | MED |
|
||||
| `continueSessionId` for targeted resume | Plumbed but server omits | Continue lands at chat root, not a session | **Not produced** by `home.ts` | LOW |
|
||||
| Quick capture panel | Present | Remove/relocate behind "+" | n/a | LOW |
|
||||
| Active models tile | Present, never populated | Remove from Home | route never sets `activeModels` | LOW |
|
||||
| J08 review banner (test-locked) | Present + correct | Preserve testids/copy/event while restyling | needsReviewCount real | (keep) |
|
||||
| Overnight-failure attention banner | Present | Reconcile with "1 export failed" run chip | failures real | LOW |
|
||||
|
||||
**Gap count (distinct design-vs-impl gaps): 11** (excludes the two "keep as-is"
|
||||
test-locked rows and the layout-only re-layout row counted once).
|
||||
|
||||
---
|
||||
|
||||
## 6. Reuse + build
|
||||
|
||||
### 6.1 Reuse (already shipped in PR1 — do NOT recreate)
|
||||
All warm-Hive tokens the design references already exist in
|
||||
`apps/web/src/index.css` and `apps/web/src/waggle-theme.css`:
|
||||
- **Surfaces/lines/text:** `--bg --bg-2 --surface --surface-2 --line --line-soft --line-strong --text-2 --text-muted --text-dim` (`index.css:149-153`).
|
||||
- **Honey + washes:** `--honey --honey-bright --honey-deep --honey-wash --honey-line --honey-glow` (`index.css:99,155-159`).
|
||||
- **Semantics + washes:** `--work --intel --healthy --attention --risk` + each `*-wash` (`index.css:161-169`).
|
||||
- **Radii:** `--r:12 --r-lg:18 --r-xl:26` (`index.css:177-179`).
|
||||
- **Shadows:** `--shadow --shadow-sm --shadow-lg --shadow-pop` (`index.css:171-174`).
|
||||
- **Fonts:** Tailwind `font-display` / `font-sans` → Hanken Grotesk, `font-mono` → JetBrains Mono (`tailwind.config.ts:87-90`). Hanken/JetBrains `@import` already in `index.css`.
|
||||
- **Utilities:** `.hex` clip-path (`waggle-theme.css:140`), `.comb` honeycomb bg (`:143-146`), `.dot-live` + `@keyframes breathe` (`index.css:428-435`), `.kbd`/mono helper (`waggle-theme.css:161`).
|
||||
- **Existing component primitives:** `WorkspaceActionsMenu` (kebab CRUD — keep on cards), `formatRelative` / `formatBriefingDate` (`HomeCockpit.tsx:69-81,130-134`), the adapter trio (`getHomeBriefing`/`getHomeOvernight`/`quickCapture`), `useOfflineStatus`, `useService().connecting` race guard, shadcn `ui/` (button/input/card available if wanted), lucide icons, `waggle:open-app` deep-link shim. ⌘K lives in `CommandCenter.tsx` (PR2) — wire the ask-bar "⌘K" hint / send to it.
|
||||
|
||||
### 6.2 Net-new (build for Editorial)
|
||||
- **Mono date row** with live dot + **🔥 streak chip** (streak value mocked/constant until a backend field exists — flag in code).
|
||||
- **Honey-keyword H1** (two-line; second line composed; honey-span helper).
|
||||
- **Overnight hero card** — a new presentational component: eyebrow "While you slept" + composed story sentence (from `OvernightSummary` counts + failures) + 4 run chips. Must render gracefully when overnight is null/empty (no crash; the test renders with `getHomeOvernight → null`).
|
||||
- **Run-chip** component (status dot + label).
|
||||
- **Hex-avatar workspace card** restyle (`.ws-card` look: hex glyph, hover lift, "Continue →", badge).
|
||||
- **`.move` suggestion row** restyle (icon tile + title + sub + sliding arrow); compose sub-line from workspace/time.
|
||||
- **Ask bar** (pill input + honey "+" + "⌘K" hint + honey send) — replaces QuickCapture as the primary input; wire send to chat/⌘K.
|
||||
|
||||
### 6.3 Recommended build approach
|
||||
1. **Keep `HomeCockpit.tsx` as the component** (route + props + load() + all five render
|
||||
states + the test-locked testids/banners stay). Restyle/replace only the **normal
|
||||
render body** (`:579-649`) and the sub-state shells to the warm Editorial look.
|
||||
2. **Container:** swap `max-w-4xl mx-auto p-6` → a centered **`max-w-[920px] mx-auto px-8 pt-[46px] pb-20`** wrapper; optionally drop a `.comb` honeycomb layer behind it.
|
||||
3. **Extract small presentational subcomponents** within the file (or co-located
|
||||
`home/` dir, per CLAUDE.md "many small files"): `GreetingHeader` (rework),
|
||||
`OvernightHero` (new, replaces `OvernightPanel`), `RunChip`, `WorkspaceCard`
|
||||
(hex restyle of the existing card), `SuggestRow` (restyle of `SuggestedActionsPanel`),
|
||||
`AskBar` (new). Keep `UpNextPanel` conditional (test 4/5) or fold it into the dash —
|
||||
simplest is to **retain it** below suggestions to satisfy the present/absent tests.
|
||||
4. **Compose, don't fetch:** the overnight **story sentence** and **run-chip labels**
|
||||
are derived client-side from the existing `OvernightSummary` + top `suggestedAction`
|
||||
/workspace summary. Mark the **streak** and **"ahead of yesterday"** as explicit
|
||||
`// TODO(backend): no data source yet` constants so the mock is honest.
|
||||
5. **Preserve behavior:** J08 banner (copy + `home-cockpit-review-cta` event payload),
|
||||
human-date, `home-cockpit` root testid, `home-cockpit-upnext` present/absent rule.
|
||||
6. **Dark-first** (per BUILD-PLAN §6); verify light via the existing ratchet
|
||||
(`light-mode-tokens.test.ts`). Gates: `tsc -p apps/web/tsconfig.app.json` 0,
|
||||
`npm run test` (FE) green incl. `p2-home-desktop.test.tsx`, lint clean.
|
||||
|
||||
> Honesty flags for the rebuild: (a) **streak** has no data anywhere — it is a pure
|
||||
> mock until a backend field lands; (b) the overnight **narrative + "9 competitors"**
|
||||
> run-chip and the suggestion **sub-lines** are composed/mocked, not server-provided;
|
||||
> (c) `continueSessionId` is still omitted server-side so "Continue" lands at chat root.
|
||||
200
docs/redesign-warm-hive/pr3-recon/primitives.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# PR3 Recon — Warm-Hive primitives & global patterns
|
||||
|
||||
> One shared vocabulary for the three PR3 screen builders (Home / Chat / Workspace).
|
||||
> Post-PR1 inventory of what already exists in `apps/web` + the design's global
|
||||
> patterns extracted from `docs/design_handoff_waggle_app/`. **Use the token names
|
||||
> and components below — do NOT invent ad-hoc colors or re-build atoms that exist.**
|
||||
|
||||
Sources of truth:
|
||||
- Warm tokens: `apps/web/src/index.css` (`@layer base`) + `apps/web/src/waggle-theme.css`
|
||||
(note: file is at `src/`, NOT `src/styles/`).
|
||||
- Tailwind utilities: `apps/web/tailwind.config.ts`
|
||||
- Design global: `docs/design_handoff_waggle_app/README.md` §6/§7 + `SCREENS.md` 01/02/03
|
||||
|
||||
---
|
||||
|
||||
## 1. Warm token cheatsheet
|
||||
|
||||
Dark is the default `:root`; `:root[data-theme="light"]` overrides every color. Radii /
|
||||
type / shadow tokens are theme-independent. Prefer the **named warm token** (`var(--x)`)
|
||||
for screen chrome; the **shadcn HSL utility** (`bg-card`, `text-foreground`) for any
|
||||
component that already derives from the HSL core.
|
||||
|
||||
### Surfaces (`apps/web/src/index.css:20-44, 149-153`)
|
||||
| Token | Dark hex | Tailwind / usage |
|
||||
|---|---|---|
|
||||
| `--bg` | `#14110b` | `bg-background` — app shell |
|
||||
| `--bg-2` | `#1a160f` | rails, recessed panels (sidebar uses `bg-[var(--bg-2)]`) |
|
||||
| `--surface` *(= `--card`)* | `#1f1a12` | `bg-card` — cards |
|
||||
| `--surface-2` *(= `--secondary`)* | `#272117` | `bg-secondary` — hover/insets |
|
||||
| `--surface-3` *(= `--muted`)* | `#322a1d` | `bg-muted` — chips, icon tiles |
|
||||
| `--line` | `#38301f` | `border-border` ≈ — default border |
|
||||
| `--line-soft` | `#2a2417` | subtle dividers (card border in design = `--line-soft`) |
|
||||
| `--line-strong` | `#4a4030` | emphasized border / kbd / scrollbar |
|
||||
|
||||
### Text (`index.css:21,36,89,153` + `waggle-theme.css:24-29`)
|
||||
| Token | Tailwind / alias |
|
||||
|---|---|
|
||||
| `--text` *(= `--foreground`)* `#f6f1e4` | `text-foreground` |
|
||||
| `--text-2` `#c8bfa9` | secondary copy |
|
||||
| `--text-muted` *(= `--muted-foreground`)* `#948a73` | `text-muted-foreground` |
|
||||
| `--text-dim` *(= `--hive-500` `#6b6250`)* | labels, meta, mono section labels |
|
||||
|
||||
### Honey — the ONE accent (`index.css:91-100, 155-159`)
|
||||
| Token | Dark hex | Notes |
|
||||
|---|---|---|
|
||||
| `--honey` *(= `--honey-500`, `--primary`)* | `#e9a52c` | `bg-primary` / `text-honey-500`; primary btn, active nav, key numbers, focus |
|
||||
| `--honey-bright` *(= `--honey-400`)* | `#f6c45a` | gradient top of hex avatar |
|
||||
| `--honey-deep` *(= `--honey-600`)* | `#c07e16` | gradient bottom of hex avatar |
|
||||
| `--honey-wash` | `rgba(233,165,44,.10)` | tinted fills (active nav bg, streak/honey chips) |
|
||||
| `--honey-line` | `rgba(233,165,44,.28)` | tinted borders (card hover, chip border) |
|
||||
| `--honey-glow` | `rgba(233,165,44,.12)` | soft glow / `--shadow-honey` |
|
||||
| **On-honey ink** | `#1a1407` | text/icon color on any honey fill (matches `--primary-foreground`) |
|
||||
|
||||
### Semantics — desaturated, STATUS ONLY (never decoration) (`index.css:161-169`)
|
||||
| Token | Dark / Light | Meaning | Wash |
|
||||
|---|---|---|---|
|
||||
| `--work` | `#7aa6d6` / `#3f72b0` | tasks, workspaces (blue) | `--work-wash` |
|
||||
| `--intel` | `#b196dd` / `#7d57b8` | memory / intelligence / **provenance** (violet) | `--intel-wash` |
|
||||
| `--healthy` | `#6cb78c` / `#3c8a5f` | complete / healthy (sage) | `--healthy-wash` |
|
||||
| `--attention` | `#e9a52c` / `#b57d12` | attention / automation (= honey) | (use `--honey-wash`) |
|
||||
| `--risk` | `#db8068` / `#c0573c` | risk / failure (terracotta) | `--risk-wash` |
|
||||
|
||||
> There is also a parallel `--sem-*` alias set (`--sem-work/-intelligence/-healthy/-attention/-risk`,
|
||||
> `index.css:110-114`) wired into the EXISTING primitives (StatusBadge, ConfidenceBadge).
|
||||
> The vivid `--status-*` / `bg-status-*` tokens (`#34d399` etc.) are the legacy palette —
|
||||
> **prefer the desaturated `--work/--intel/...` (or `--sem-*`) for warm-Hive screens.**
|
||||
|
||||
### Shadows / radii / fonts (`index.css:171-183`)
|
||||
| Token | Value |
|
||||
|---|---|
|
||||
| `--shadow-sm` / `--shadow` / `--shadow-lg` / `--shadow-pop` | card → overlay elevation |
|
||||
| `--shadow-honey` | `0 0 0 1px rgba(233,165,44,.25), 0 8px 30px -10px rgba(233,165,44,.35)` |
|
||||
| `--r-sm` 8px · `--r` 12px · `--r-lg` 18px · `--r-xl` 26px · pills `999px` | `rounded-sm/md/lg/xl` map to the shadcn radius scale, NOT these raw px — use `rounded-[18px]`/`rounded-[var(--r-lg)]` for design-exact cards |
|
||||
| `--sans` Hanken Grotesk · `--mono` JetBrains Mono | `font-sans` / `font-mono`; `--serif` = `--sans` (no book-serif) |
|
||||
|
||||
> Caveat for builders: Tailwind `rounded-lg` = `--radius` (0.75rem/12px = design `--r`),
|
||||
> NOT `--r-lg` (18px). Design **cards** want 18px → use `rounded-[18px]`. Design hero
|
||||
> card wants 26px → `rounded-[26px]`. Pills → `rounded-full`.
|
||||
|
||||
### Utility classes already shipped (`index.css` + `waggle-theme.css`)
|
||||
- `.hex` (`waggle-theme.css:140`) — hex clip-path for brandmark/avatars/tiles.
|
||||
- `.hex-avatar` (`index.css:377`) — same clip path (duplicate; either works).
|
||||
- `.comb` (`waggle-theme.css:143`) + `.honeycomb-bg` (`index.css:369`) — subtle hex mesh bg.
|
||||
- `.dot-live` → `@keyframes breathe` 2.4s (`index.css:429-435`) — live/active status dot.
|
||||
- `.heartbeat` (2s), `.honey-pulse`, `.float`, `.hex-cursor` (streaming type cursor),
|
||||
`.token-stream`, `.send-flash`, `.card-enter`, `.hex-spin` — all in `index.css:408-503`
|
||||
and mirrored as Tailwind `animate-*` in `tailwind.config.ts:107-156`.
|
||||
- `.pill` (`waggle-theme.css:151`) — status/filter chip base.
|
||||
- `.kbd` (`waggle-theme.css:160`) — keyboard hint chip.
|
||||
- `.glass` / `.glass-strong` / `.glow-primary` / `.text-glow` (`index.css:334-358`).
|
||||
- `.waggle-card-lift` (`waggle-theme.css:112`) — hover `translateY(-2px)` + honey border
|
||||
(matches design card hover). `.waggle-interactive`, `.waggle-nav-hover`, `.waggle-press`.
|
||||
- Focus: global `:focus-visible { outline: 2px solid var(--honey-500); offset 2px }`
|
||||
(`index.css:389`). Selection = honey @ .28 (`index.css:383`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Existing reusable components (REUSE — do not rebuild)
|
||||
|
||||
| Component | Path | Props | Reuse for which PR3 screen |
|
||||
|---|---|---|---|
|
||||
| `ConfidenceBadge` | `apps/web/src/components/ui/confidence-badge.tsx:22` | `value?: number; compact?: boolean; className?` — 0-100 → High/Med/Low band via `--sem-*`, never color-only | Workspace "What Waggle knows" fact rows; Chat memory-write steps |
|
||||
| `EvidenceChip` | `apps/web/src/components/ui/evidence-chip.tsx:16` | `label: string; title?; onClick?; className?` — the inline provenance pill promoted to a primitive | Chat activity-stream provenance pills; Workspace fact/artifact `⬡ source · when` |
|
||||
| `EvidencePanel` | `apps/web/src/components/ui/evidence-panel.tsx:18` | `source?; sourceId?; sourceUrl?; evidence?: string[]; className?` — "Provenance & evidence" block of chips | Workspace memory detail; Chat memory-write detail |
|
||||
| `StatusBadge` | `apps/web/src/components/ui/status-badge.tsx:31` | `tone: 'healthy'\|'attention'\|'risk'\|'info'\|'neutral'; label: string; icon?; className?` — always-labeled `--sem-*` pill | Home run-chip status dots, Workspace status card, all status pills |
|
||||
| `ApprovalModal` | `apps/web/src/components/ui/approval-modal.tsx:60` | `request: ApprovalRequest\|null; approveLabel?; busy?; onApprove; onCancel` (`ApprovalRequest = {action, scope[], riskLevel, approvalClass?, trustSource?}`) | Chat **inline approval card** is a different surface — but reuse this modal for the same flow + share `RISK_LABELS`/`risk-display.tsx` |
|
||||
| `DetailDrawer` | `apps/web/src/components/ui/detail-drawer.tsx:22` | `open; onOpenChange; title; subtitle?; headerExtra?; footer?; children; className?` — right sheet for object detail | Workspace fact/artifact detail; Chat artifact detail |
|
||||
| `BuilderStepper` | `apps/web/src/components/ui/stepper.tsx:29` | `steps: BuilderStep[]; ...` body-portaled focus-trapped modal stepper | not core to Home/Chat/Workspace; available |
|
||||
| shadcn primitives | `apps/web/src/components/ui/` | card, button, badge, tabs, dialog, tooltip, popover, command, scroll-area, separator, avatar, input, textarea, switch, sheet, sonner/toast, hover-card, dropdown-menu, alert-dialog, progress, skeleton, table, +30 more | Workspace **tab bar** = `tabs.tsx`; ⌘K already on `command.tsx`; cards/buttons everywhere; toasts via `sonner` |
|
||||
| `Sidebar` (calm spine) | `apps/web/src/components/os/Sidebar.tsx:44` | `workspaceName; spine: SidebarNavItem[]; pinned?; onOpen*; userName; tierLabel` — already implements **active = left honey bar + `--honey-wash` + honey icon** (`Sidebar.tsx:82-91`) and the **hex avatar** inline (`:116`) | Already the shell; reuse its active-state recipe + hex pattern verbatim |
|
||||
| ⌘K catalog | `apps/web/src/lib/command-catalog.ts:58` | `buildCommandCatalog(ctx)` → plain-name + mono-subtitle groups (Pinned/Jump/Do/Power) | Chat/Home "⌘K" hints route here; don't re-author the vocabulary |
|
||||
|
||||
> shadcn `button.tsx` `default` variant = `bg-primary text-primary-foreground` = honey
|
||||
> bg / `#1a1407` ink → already matches the design primary button. `ghost`/`outline`
|
||||
> variants cover the design "ghost" button. shadcn `badge.tsx` is **color-capable but
|
||||
> not always-labeled** — prefer `StatusBadge` when conveying status (a11y).
|
||||
|
||||
---
|
||||
|
||||
## 3. Missing primitives to build in PR3 (NET-NEW shared components)
|
||||
|
||||
These appear across Home/Chat/Workspace and have **no component today** (the hex avatar
|
||||
exists only as inline markup in `Sidebar.tsx:116`). Build them once as shared atoms.
|
||||
**Suggested home: `apps/web/src/components/os/warm/`** (new folder for warm-Hive-specific
|
||||
composite atoms) — keep generic, token-driven, a11y-labeled primitives in `ui/` and the
|
||||
opinionated warm compositions in `os/warm/`.
|
||||
|
||||
| Net-new primitive | Where | Minimal prop API | Used by |
|
||||
|---|---|---|---|
|
||||
| `HexAvatar` | `os/warm/HexAvatar.tsx` | `label: string; size?: number; gradient?: boolean; className?` — `.hex` clip + honey gradient `linear-gradient(150deg,var(--honey-bright),var(--honey-deep))` + `#1a1407` initial; extract from `Sidebar.tsx:116` | workspace switcher, Home workspace cards, Chat context header, Workspace header (46px), bot avatar |
|
||||
| `DotLive` | `os/warm/DotLive.tsx` | `tone?: 'healthy'\|'attention'\|'risk'\|'work'\|'intel'\|'honey'; className?` — colored dot + `.dot-live` breathe; honor `prefers-reduced-motion` | Home greeting live dot, Workspace "1 agent live", status cards |
|
||||
| `RunChip` | `os/warm/RunChip.tsx` | `label: string; tone?: StatusTone` — status dot + label inline chip ("Teardown drafted · 9 competitors") | Home overnight hero run-chip row |
|
||||
| `StreakChip` | `os/warm/StreakChip.tsx` | `days: number; weekDots?: boolean[]; className?` — 🔥 + "12-day streak", `--honey-wash` bg / `--honey-line` border | Home greeting (§ habit-loop mechanic lives on Home, not a page) |
|
||||
| `SectionLabel` | `os/warm/SectionLabel.tsx` | `children; className?` — 11px mono, uppercase, `.12-.14em` tracking, `--text-dim`, trailing hairline rule | every screen section header (README §6) |
|
||||
| `ScreenHead` (`.shead`) | `os/warm/ScreenHead.tsx` | `title; subtitle?; action?: ReactNode` — H1 (Hanken 600 ~24-28px) + subtitle + primary action row | Workspace header; power surfaces; generic screen chrome (README §6 / SCREENS §08) |
|
||||
| `ProvenanceLine` | `os/warm/ProvenanceLine.tsx` | `source: string; when?: string; onClick?` — `⬡ source · when` in mono `--intel` | Workspace fact rows + recent-work rows; Chat activity steps (thin wrapper over `EvidenceChip` styled to `--intel` mono) |
|
||||
| `ModelPill` | `os/warm/ModelPill.tsx` | `mode?: string; model: string; onClick?` — "auto · Claude Sonnet" pill in Chat header | Chat context header |
|
||||
| `ActivityStream` | `os/warm/ActivityStream.tsx` | `summary: string; durationMs?; steps: {tone, text, provenance?}[]; defaultOpen?` — collapsible `--bg-2` card, violet spark, per-step colored dot + `ProvenanceLine` | Chat "the magic" activity card (default-open on active turn) |
|
||||
| `InlineApprovalCard` | `os/warm/InlineApprovalCard.tsx` | reuse `ApprovalRequest`; `onApprove; onDecline; alwaysAllow?` — `--honey-wash` bg, attention border, warning icon (NOT a modal — inline in thread) | Chat approval card (shares risk vocab w/ `ApprovalModal`) |
|
||||
| `OvernightHero` | `os/warm/OvernightHero.tsx` (composite, Home-only) | `eyebrow; statement: ReactNode; runs: RunChipProps[]` — `--r-xl` gradient card + honey radial glow | Home (composes RunChip) |
|
||||
| `AskBar` | `os/warm/AskBar.tsx` | `placeholder?; onSubmit; cmdkHint?: boolean` — full-width pill, honey `+`, ⌘K hint, honey send | Home (and Chat composer reuses the send affordance) |
|
||||
| `HexCheckTile` | `os/warm/HexCheckTile.tsx` | `tone?; size?` — small `.hex` tile w/ check, for fact rows | Workspace "What Waggle knows" fact rows |
|
||||
| `IconTile` | `os/warm/IconTile.tsx` | `icon: ElementType; tone?: StatusTone` — tinted (`*-wash`) rounded square icon tile | Home "Waggle suggests" rows; Workspace recent-work ext tiles |
|
||||
|
||||
> Build order suggestion: `HexAvatar`, `SectionLabel`, `ProvenanceLine`, `DotLive`,
|
||||
> `RunChip`, `IconTile` first (shared by all three screens), then the screen-specific
|
||||
> composites. Keep each <80 LOC, token-driven, `prefers-reduced-motion`-safe.
|
||||
|
||||
---
|
||||
|
||||
## 4. Global pattern rules (apply on every PR3 screen)
|
||||
|
||||
From `README.md` §6 and `SCREENS.md`:
|
||||
|
||||
1. **Density / scale:** body 16px / line-height 1.55 (set on `body`, `index.css:321`);
|
||||
**never below 12px**. Honey words in headlines are honey-colored, **not italic**,
|
||||
same weight. Headlines Hanken 600, `letter-spacing -0.02em` (already on `h1-h6`,
|
||||
`index.css:327`).
|
||||
2. **Section labels:** 11px **mono**, uppercase, `.12-.14em` tracking, `--text-dim`,
|
||||
trailing hairline rule. (Sidebar zone label is the reference: `Sidebar.tsx:102`,
|
||||
`9.5px mono uppercase tracking-[0.14em] text-[var(--text-dim)]`.)
|
||||
3. **One accent — honey, sparingly:** primary buttons, active nav, **key numbers**,
|
||||
focus only. Semantics are desaturated and status-only, never decoration.
|
||||
4. **Cards:** `bg-card` (`--surface`), `1px solid` **`--line-soft`**, radius **18px**
|
||||
(`--r-lg`), hover → `--honey-line` border + `translateY(-2px)` + `--shadow`. Use
|
||||
`.waggle-card-lift` for the hover recipe.
|
||||
5. **Active nav state:** **left honey bar** (`absolute -left-3 h-[18px] w-[3px] bg-[var(--honey)]`)
|
||||
+ `--honey-wash` bg + honey icon — already implemented in `Sidebar.tsx:82-91`; mirror
|
||||
on the Workspace tab bar as a **honey underline** on the active tab.
|
||||
6. **Provenance everywhere:** any memory / fact / artifact shows `⬡ source · when` in
|
||||
**mono / `--intel`**. Core trust pattern — never drop it. Use `ProvenanceLine` /
|
||||
`EvidenceChip`.
|
||||
7. **Buttons:** primary = honey bg / `#1a1407` ink (shadcn `default`); ghost = `bg-card`
|
||||
/ `--line-strong` border, hover honey border. Radius 9-13px.
|
||||
8. **Hex motif:** brandmark + agent/workspace avatars + icon tiles use `.hex` clip with
|
||||
the honey gradient. Honeycomb (`.comb`/`.honeycomb-bg`) is decorative-only at ~.05.
|
||||
9. **Motion:** entrances `cubic-bezier(.16,1,.3,1)` ~.7s, **content visible without JS /
|
||||
reduced-motion**. Hover transitions .14-.18s. Live status = `.dot-live` (breathe 2.4s).
|
||||
**Honor `prefers-reduced-motion`** on every animated atom.
|
||||
10. **Focus / a11y:** `2px solid --honey`, 2px offset (global). Status is **never
|
||||
color-only** — always a text label (StatusBadge/ConfidenceBadge pattern). Keep
|
||||
keyboard access on ⌘K, tabs, composer.
|
||||
11. **Dark + light:** both warm (graphite ↔ paper) via `:root[data-theme="light"]`;
|
||||
drive through the app theme provider (`apps/web/src/providers/ThemeProvider.tsx`),
|
||||
NOT localStorage scaffolding. Every new color must resolve from a token so light
|
||||
mode inherits for free (light-mode AA is test-guarded).
|
||||
|
||||
### Per-screen anchor (ship-variation)
|
||||
- **Home → Variation A (Editorial):** centered column max-w 920px; greeting (mono date +
|
||||
DotLive + StreakChip) → OvernightHero (run chips) → "Pick up where you left off" 2-col
|
||||
workspace cards (HexAvatar) → "Waggle suggests" rows (IconTile) → AskBar. (`SCREENS.md` 01)
|
||||
- **Chat → Variation B (Split work canvas):** context header (HexAvatar + name + memory
|
||||
count + ModelPill) → thread (user bubble asymmetric radius `4px 14px 14px 14px`; bot
|
||||
hex avatar; ActivityStream w/ ProvenanceLine; InlineApprovalCard) → composer; right
|
||||
work-canvas (~42%) with `.hex-cursor` live draft. (`SCREENS.md` 02)
|
||||
- **Workspace → Variation A (Overview + tabs, Memory = a tab):** ScreenHead (breadcrumb +
|
||||
46px HexAvatar + meta) → Tabs (Overview/Chat/Memory/Artifacts/Files/Team, honey
|
||||
underline) → 2-col (1.7fr/1fr): left = summary + "What Waggle knows" (HexCheckTile +
|
||||
ProvenanceLine fact rows) + "Recent work" (IconTile rows); right = Status card +
|
||||
Up-next + Team. **Do NOT make the graph the default.** (`SCREENS.md` 03)
|
||||
276
docs/redesign-warm-hive/pr3-recon/workspace.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# PR3 Recon — SCREEN 03 · Workspace (Variation A: Overview + tabs)
|
||||
|
||||
Maps the **current** Workspace surface against the warm-Hive design (`workspace.html`,
|
||||
`SCREENS.md` §03). Ship target = **Variation A "Overview + tabs"**, **Memory stays a
|
||||
tab** (do NOT make the knowledge-graph the default — that's the alternate Variation B,
|
||||
deferred).
|
||||
|
||||
Primary file: `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx` (1020 LOC).
|
||||
Route wrapper: `apps/web/src/routes/WorkspaceRoute.tsx`.
|
||||
Design ref: `docs/design_handoff_waggle_app/design-files/screens/workspace.html`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Current structure (with line refs)
|
||||
|
||||
`WorkspaceDesktopApp.tsx` is a fixed-layout shell (no drag/resize grid, per its header
|
||||
comment lines 9-21). Composition top-to-bottom:
|
||||
|
||||
- **Tab model** — 8 tabs, `WorkspaceTabId` union + `TABS` array
|
||||
(`WorkspaceDesktopApp.tsx:58-77`): `overview · chat · research · artifacts · memory ·
|
||||
tasks · timeline · settings`. Each tab is `{ id, label, icon }` (lucide icons). **No
|
||||
per-tab counts.**
|
||||
- **Header** (`:759-811`) — flat row: `<h2>` workspace name (`:761`), a **type pill**
|
||||
(`:762-766`), a **status pill** (`statusPillClass`, `:767-772` + helper `:117-126`),
|
||||
an inline "N running" agent indicator (`:773-778`), a **members avatar stack**
|
||||
(`:783-798`, initials, max 5 + overflow), and the **`WorkspaceActionsMenu`**
|
||||
(`:800-809`, rename/archive/restore/export/delete). **No breadcrumb. No hex avatar.
|
||||
No meta row** (memories/sources/updated). **No "Memory"/"Continue" header buttons.**
|
||||
- **Tab bar** (`:814-839`) — horizontal scroll nav, `role=tablist`. Active tab =
|
||||
`border-primary` bottom border + `text-foreground` (`:827-831`); honey comes via the
|
||||
`--primary` token so the underline is already honey-tinted. Icon + label per tab, **no
|
||||
count badges**.
|
||||
- **Body = main canvas + right context panel** (`:842-969`):
|
||||
- **Overview** (`OverviewTab`, `:518-545`) — a **widget grid**
|
||||
(`grid-cols-1 md:grid-cols-2 xl:grid-cols-3`, fixed 16rem row height, `:530-533`):
|
||||
5 `WidgetCard`s — `ChatPreviewWidget` (read-only thread preview, `:184-232`),
|
||||
`ArtifactsWidget` (file registry, `:241-283`), `TasksWidget` (pending+blocked from
|
||||
state, `:286-333`), `MemoryHighlightsWidget` (decisions + "I remember", `:335-377`),
|
||||
`ActivityWidget` (audit feed, `:379-401`). Empty-state when everything empty
|
||||
(`:850-868`).
|
||||
- **Other tabs** — `chat` renders the `chatSlot` or a deep-link placeholder
|
||||
(`:882-902`); `tasks` → `<TasksTab>` (`:904`); `memory` →
|
||||
`<MemoryCenterTab mind="workspace">` (`:937-941`); `research`/`timeline`/`settings`
|
||||
are `<TabPlaceholder>` stubs (`:906-957`); `artifacts` is an inline file-card grid
|
||||
or placeholder (`:914-932`).
|
||||
- **Right context panel** (`WorkspaceInfoPanel`, `:405-514`, `hidden lg:flex w-72`):
|
||||
Workspace block (name/desc + memory/session counts, `:422-439`), **Team members**
|
||||
(`:441-460`, global roster — see §2), **Last activity** + "N agents running"
|
||||
(`:462-481`), **Quick actions** (Open chat / View memory / Review tasks, `:483-511`).
|
||||
- **Whole-screen states** — loading (`:691-698`), `permission`/`notfound`/`offline`
|
||||
error variants (`:700-754`) via `FullScreenState` (`:1000-1018`), empty-workspace
|
||||
(`:850-868`).
|
||||
- **Route wrapper** (`WorkspaceRoute.tsx`) — `/workspaces/:workspaceId/:tab?`; the URL
|
||||
drives `activeTab` (controlled seam §5.2a, `:43-54`); `onTabChange` navigates
|
||||
(overview = bare `/workspaces/:id`, `:52-53`); `chatSlot` portals the live chat
|
||||
(`:57`); URL→shell sync effect (`:34-38`). The 8 tabs are pinned in `WS_TABS`
|
||||
(`:20-22`).
|
||||
- **`WorkspaceBriefing.tsx`** (284 LOC) — a SEPARATE component, shown **inside ChatApp**
|
||||
when a thread has no messages (header comment `:1-5`). Reads the SAME
|
||||
`getWorkspaceContext` (`:57`). Holds "I Remember" / "Recent Decisions" / "Recent
|
||||
Conversations" / suggested-prompt chips / stats bar. **It is not part of the Workspace
|
||||
Desktop shell** — it's the chat empty-state. Useful as a copy/data reference for the
|
||||
Overview's "What Waggle knows" rows but is not the screen being rebuilt. Its
|
||||
collapsed-state persistence is `lib/workspace-briefing-state.ts` (tested — see §3).
|
||||
|
||||
---
|
||||
|
||||
## 2. Data contract — what's REAL vs mocked
|
||||
|
||||
The Overview's three data feeds come from real, populated endpoints (verified in
|
||||
`packages/server/src/local/routes/workspaces.ts`). The DESIGN, however, surfaces several
|
||||
fields the contract does NOT yet provide. Table below; "✔ real" = endpoint returns it
|
||||
today, "✖ mock" = design shows it but no field exists, "~ derivable" = computable from
|
||||
existing data.
|
||||
|
||||
### Endpoints + adapter methods (all real, no envelope)
|
||||
| Adapter (`lib/adapter.ts`) | Route (`workspaces.ts`) | Returns |
|
||||
|---|---|---|
|
||||
| `getWorkspaceContext(id)` `:582` | `GET /:id/context` `workspaces.ts:364-655` | summary, recentMemories, recentDecisions, recentThreads, suggestedPrompts, `stats{memoryCount,sessionCount,fileCount}`, greeting, pendingTasks, workspace{type,status,description} |
|
||||
| `getWorkspaceState(id)` `:601` | `GET /:id/state` `:663-693` | active/openQuestions/**pending**/**blocked**/completed/stale/recentDecisions/nextActions (WorkspaceStateView) |
|
||||
| `getWorkspaceActivity(id,limit)` `:606` | `GET /:id/activity` `:699-742` | `{events:[{id,ts,type,actor?,summary}]}` from audit_events |
|
||||
| `getWorkspaceFiles(id)` `:587` | `GET /:id/files` `:744-756` | `{files:[…]}` from file registry (interim "artifacts") |
|
||||
| `getTeamMembers()` `:2160` | (global team roster) | `[{id,name,status,avatar?}]` — **NOT workspace-scoped** (`WorkspaceDesktopApp.tsx:660-666` TODO) |
|
||||
|
||||
### Design field → reality
|
||||
| Design element (workspace.html / SCREENS §03) | Source | Status |
|
||||
|---|---|---|
|
||||
| **Header meta: "142 memories"** | `ctx.stats.memoryCount` | ✔ real |
|
||||
| **Header meta: "9 sources"** | — | ✖ mock (no `sourceCount`; harvest sources exist in mind but not exposed on context) |
|
||||
| **Header meta: "1 agent live"** | `useRoomState().workspaceMap.get(id).live.length` (`:603-607`) | ✔ real (live SSE) |
|
||||
| **Header meta: "updated 2h ago"** | `ctx.lastActive` (`:592,646`) | ~ derivable (have `lastActive`; relativeTime helper exists `:135-147`) |
|
||||
| **Tab counts (Chat 3 · Memory 142 · Artifacts 7 · Files 12 · Team 4)** | memory=`stats.memoryCount` ✔; sessions=`stats.sessionCount` ✔; files=`stats.fileCount` ✔; team=`getTeamMembers().length` ✔(global); **artifacts** ✖ (no distinct artifact entity — interim = files) | ~ mostly derivable; artifacts count is the gap |
|
||||
| **Left: summary card** | `ctx.summary` (`composeWorkspaceSummary` `workspaces.ts:31-75`) | ✔ real |
|
||||
| **"What Waggle knows" fact rows** | `ctx.recentMemories` (content+importance+date) and/or `ctx.recentDecisions` | ✔ content real; **✖ per-fact provenance `⬡ source · when`** (frames have no `source` surfaced — design's "web · mem0.ai", "teardown.md" are mock; `date` IS present so "·when" is real) |
|
||||
| **"Recent work" artifact rows (ext tile + name + provenance + time)** | `getWorkspaceFiles` → `normalizeArtifacts` (`:977-998`): name, mimeType/modifiedAt | ✔ name+time real; **✖ provenance source** ; ext-tile derivable from filename |
|
||||
| **Right Status: agent live + name** | `useRoomState` live list — name not in the view-model today | ~ "live" real, agent NAME ✖ mock |
|
||||
| **Right Status: model "auto · Claude Sonnet"** | `ctx.workspace.model` (`workspaces.ts:625`) | ✔ real (model id; "auto" + friendly name is display) |
|
||||
| **Right Status: "+6 today"** | — | ✖ mock (no per-day memory delta on context; overnight delta exists on Home `OvernightSummary.consolidated` but not per-workspace) |
|
||||
| **Right Status: "3 to review / Needs review"** | `HomeBriefing.needsReviewCount` exists at HOME scope (J08); per-workspace ✖ | ✖ mock at workspace scope |
|
||||
| **Right "Up next" rows** | `state.pending` + `state.blocked` + `state.nextActions` + cron `upcomingSchedules` | ~ derivable (TasksWidget already uses pending/blocked; nextActions+schedules unused on this screen) |
|
||||
| **Right Team avatar rows** | `getTeamMembers()` | ✔ real but **global roster, not per-workspace** (documented TODO `:660-662`) |
|
||||
|
||||
**Net:** the three load-bearing columns (summary, knowledge/memory facts, recent
|
||||
work/artifacts, status counts, up-next, team) are all **backed by real endpoints**. The
|
||||
**provenance `⬡ source · when` line** (a core trust pattern, README §6) is the single
|
||||
biggest data gap — memory frames have a `source` column server-side
|
||||
(`workspaces.ts:291` writes `source`, `:413` SELECTs around it) but it is **not
|
||||
projected into `recentMemories`/`recentDecisions`** today, nor onto file rows. "9
|
||||
sources", "+6 today", per-workspace "N to review", and a distinct **artifacts** entity
|
||||
(vs files) are genuinely absent.
|
||||
|
||||
---
|
||||
|
||||
## 3. Test contract
|
||||
|
||||
What PR3 must NOT break (existing tests touching this surface):
|
||||
|
||||
- **`apps/web/src/test/p1a-workspace-route.test.tsx`** — pins the **route ↔ shell
|
||||
contract**, NOT the visual layout. Asserts `WorkspaceRoute` calls
|
||||
`selectWorkspace(routedId)` on deep-link / Back-Forward, skips when already active,
|
||||
and **never** syncs the `local-default` placeholder (`:56-80`). It mocks
|
||||
`WorkspaceDesktopApp` to a stub (`:30-32`) and `ChatHost`/`ChatSlot` (`:33-36`). The
|
||||
route renders under `path="workspaces/:workspaceId/:tab?"` (`:44`). **Constraint:** the
|
||||
`:tab?` param, the `selectWorkspace`-on-route effect, and the `local-default` guard
|
||||
must survive any rewrite of `WorkspaceRoute`.
|
||||
- **`apps/web/src/lib/workspace-briefing-state.test.ts`** — pins the per-workspace
|
||||
briefing-collapsed localStorage helpers (key prefix `waggle:workspace-briefing-
|
||||
collapsed:`, sanitisation, per-id isolation, no colon-boundary leak). Only relevant if
|
||||
the briefing collapse behavior is carried into the new Overview; the helpers themselves
|
||||
can be reused as-is.
|
||||
- **Implicit `data-testid` contract** (consumed by live-smoke / Playwright + the empty
|
||||
flows): `ws-desktop-root`, `ws-tab-bar`, `ws-tab-<id>`, `ws-tab-panel`,
|
||||
`ws-overview-grid`, `ws-widget-{chat,artifacts,tasks,memory,activity}`,
|
||||
`ws-info-panel`, `ws-status-pill`, `ws-members-stack`, `ws-agents-running`,
|
||||
`ws-desktop-{loading,permission-denied,notfound,offline,retry}`, `ws-memory-tab`.
|
||||
Changing the tab set (8→6) removes `ws-tab-{research,tasks,timeline,settings}` — grep
|
||||
for those test ids before deleting (none appear in the two test files above, so the
|
||||
risk is in untracked Playwright smokes, not unit tests).
|
||||
|
||||
There is **no test that pins the 8-tab set, the widget-grid layout, or the right-panel
|
||||
contents** — so the Overview re-layout (grid → 2-col 1.7fr/1fr) and the tab reduction
|
||||
are free to change as long as the route/shell contract and the load-bearing test ids are
|
||||
preserved.
|
||||
|
||||
---
|
||||
|
||||
## 4. Design spec — Variation A (exact copy + structure)
|
||||
|
||||
From `SCREENS.md` §03 + `workspace.html` Variation A markup:
|
||||
|
||||
**Header** (`workspace.html:138-155`):
|
||||
- **Breadcrumb** (`.crumbs`, mono 11.5px, `--text-dim`): `Home › **Competitive
|
||||
Intelligence**` (current workspace bold).
|
||||
- **46px hex avatar** (`.wmark.hex`, 46×52, honey gradient `--honey-bright → --honey-deep`,
|
||||
`#1a1407` glyph) showing the workspace initial.
|
||||
- **H1** title (Hanken 650, 28px, `-0.02em`).
|
||||
- **Meta row** (`.wmeta`, 13px `--text-muted`, gap 14px): `● 1 agent live` (live dot
|
||||
`--healthy`) · `142 memories` · `9 sources` · `updated 2h ago`.
|
||||
- **Header actions** (right): `Memory` (ghost button) + `Continue →` (honey primary).
|
||||
|
||||
**Tab bar** (`.tabs`, `workspace.html:157-164`): `Overview · Chat 3 · Memory 142 ·
|
||||
Artifacts 7 · Files 12 · Team 4`. Active tab = `--text` + **2px honey bottom-border**
|
||||
(`.tab.on`, `:44`). Each count is a mono 11px `--text-dim` `.cnt` span.
|
||||
|
||||
**Overview content** — 2-col grid `1.7fr / 1fr`, gap 22px (`.grid`, `:49`):
|
||||
|
||||
- **Left col:**
|
||||
- **Summary card** (`.card`, `.summary` 15.5px/1.6, honey-bold keywords). Exact sample
|
||||
copy: *"This workspace tracks the **persistent-memory competitor landscape** for the
|
||||
Q2 board cycle. Waggle has mapped **9 rivals**, pulled current pricing, and drafted a
|
||||
teardown — the live thread is mid-flight on turning the opening into a board brief."*
|
||||
- **"What Waggle knows"** section (`.sec-h` mono uppercase label + brain icon, `:177`)
|
||||
→ `.knows` list of `.fact` rows (`:61-67`): each = a **30px hex check tile**
|
||||
(honey-gradient, `#1a1407` checkmark) + fact text (honey-bold spans) + a
|
||||
**provenance line** `⬡ **source** · when` (`.prov`, mono 10.5px, `.src` =
|
||||
`--intel`). Sample facts (`:279-284`): "Mem0 is cloud-only and raised prices ~15%…"
|
||||
(`web · mem0.ai · 2h ago`), "Only 2 of 9 rivals ship local-first memory."
|
||||
(`teardown.md · 2h ago`), "Mara wants the board brief to lead with the regulated-
|
||||
industries opening." (`chat · Tue · 2d ago`), etc.
|
||||
- **"Recent work"** section (file icon, `:182`) → `.arts` list of `.art` rows
|
||||
(`:71-78`): **32px ext tile** (tinted by type color, mono ext label e.g. `MD`/`XLS`/
|
||||
`PDF`) + name (`<b>`) + subtitle + right-aligned mono `.when` time. Samples (`:288-291`):
|
||||
`teardown.md` "Q2 competitive teardown · 9 competitors" `2h ago`,
|
||||
`pricing-landscape.xlsx`, `mem0-teardown.pdf`. Rows hover → `--honey-line` border.
|
||||
|
||||
- **Right col** (three `.card`s):
|
||||
- **Status** (`:188-194`): `.stat-line` rows — `Agent` → `● Research-synth · live`
|
||||
(`--healthy`); `Model` → `auto · Claude Sonnet`; `Memories` → `142 **+6 today**`
|
||||
(delta `--healthy`); `Needs review` → `3 memories` (`--attention`).
|
||||
- **Up next** (`:196-201`): `.agentrow`s — colored dot + label + mono status: `Board
|
||||
brief from teardown` (draft, `--work`); `Export table → Salesforce` (awaiting you,
|
||||
`--attention`); `Weekly digest` (`17:00`, `--healthy`).
|
||||
- **Team** (`:203-206`): `.person` rows — 30px round avatar (initial, colored bg) +
|
||||
name + role: `Mara K. · Owner`, `Research-synth · Agent · live`, `Deck-builder ·
|
||||
Agent · idle`, `Jonas P. · Editor`.
|
||||
|
||||
**Non-Overview tabs** in this concept pass are a single empty placeholder (`.emptytab`,
|
||||
`:211-216`): *"This tab is wired in the full prototype — Overview is the focus of this
|
||||
concept pass."* — i.e. the design only fully specs Overview; the other tabs route to the
|
||||
**real existing screens** (see §6).
|
||||
|
||||
**Memory stays a tab** (SCREENS §03 emphatic, README §4/§5 table). Variation B
|
||||
(memory-forward knowledge-graph as the default view, `workspace.html:219-265`) is an
|
||||
**alternate to defer**, NOT this PR.
|
||||
|
||||
---
|
||||
|
||||
## 5. Gap table (current → design)
|
||||
|
||||
| # | Area | Current | Design (Var A) | Severity |
|
||||
|---|------|---------|----------------|----------|
|
||||
| G1 | **Tab set** | 8 tabs: Overview/Chat/**Research**/Artifacts/Memory/**Tasks**/**Timeline**/**Settings** (`:68-77`) | 6 tabs: Overview/Chat/**Memory**/Artifacts/**Files**/**Team** | HIGH — drop Research/Tasks/Timeline/Settings from the bar; add Files + Team; reorder |
|
||||
| G2 | **Tab counts** | none | per-tab mono count (Chat N·Memory N·Artifacts N·Files N·Team N) | MED — wire from `stats` + members; artifacts count gap |
|
||||
| G3 | **Header avatar** | none | 46px hex avatar, honey gradient, workspace initial | MED — reuse `.hex` clip-path + initialsOf |
|
||||
| G4 | **Breadcrumb** | none | `Home › Workspace` mono crumb | LOW |
|
||||
| G5 | **Header meta row** | type pill + status pill + "N running" | `● agent live · N memories · N sources · updated Xago` | HIGH — restyle to meta row; "sources" is a data gap (§2) |
|
||||
| G6 | **Header actions** | WorkspaceActionsMenu (kebab) | `Memory` ghost + `Continue →` honey | MED — add the two buttons; KEEP the actions menu (real feature, not in mock) |
|
||||
| G7 | **Overview layout** | 3-col equal-height widget grid | 2-col 1.7fr/1fr: left summary+knows+recent-work, right status+upnext+team | HIGH — full re-layout |
|
||||
| G8 | **"What Waggle knows" rows** | `MemoryHighlightsWidget` (plain list, no provenance, no hex tile) | `.fact` rows: hex check tile + honey-bold text + `⬡ source · when` provenance | HIGH — new row component; **provenance source is a data gap** |
|
||||
| G9 | **"Recent work" rows** | `ArtifactsWidget` (file icon + name) | `.art` rows: ext tile + name + subtitle + mono time + hover border | MED — restyle; ext-tile + provenance |
|
||||
| G10 | **Status card** | scattered in right panel (counts + last activity) | one Status card: agent/model/memories+delta/needs-review | MED — consolidate; "+6 today" & "needs review" are data gaps |
|
||||
| G11 | **Up next card** | none (tasks live in a widget + Tasks tab) | `.agentrow` list (draft/awaiting-you/scheduled) | MED — derivable from pending/blocked/nextActions/schedules |
|
||||
| G12 | **Team card** | right-panel "Team members" (global roster) | Team card with role labels (Owner/Agent·live/Editor) | LOW — restyle; still global-roster-backed |
|
||||
| G13 | **Right context panel** | persistent `w-72` aside (info/team/last-activity/quick-actions) | folded INTO the Overview right column; no separate aside | MED — the aside's content moves into the grid's right col |
|
||||
| G14 | **Provenance pattern** | absent on this screen | `⬡ source · when` on every fact + artifact (core trust pattern) | HIGH (trust) — needs `source` projected from frames (§2) |
|
||||
| G15 | **Tokens/typography** | shadcn `text-foreground/muted-foreground`, `font-display`, 10-12px dense | warm tokens, Hanken H1 28/650, 13-16px body, mono labels | MED — apply warm-Hive tokens (already shipped, §6) |
|
||||
| G16 | **Memory tab** | `<MemoryCenterTab mind="workspace">` already embedded (`:937-941`) | Memory stays a tab | ✅ already correct — keep |
|
||||
|
||||
---
|
||||
|
||||
## 6. Reuse + build — tab bar → existing routes
|
||||
|
||||
The 6 design tabs map cleanly onto surfaces that **already exist**; PR3 rebuilds the
|
||||
SHELL + Overview, and the other 5 tabs embed/deep-link the real screens:
|
||||
|
||||
| Design tab | Maps to | Exists? | How to wire |
|
||||
|---|---|---|---|
|
||||
| **Overview** | `WorkspaceDesktopApp` Overview canvas | ✔ (re-layout) | Rebuild as 2-col grid; this is the bulk of PR3 |
|
||||
| **Chat** | `ChatSlot` (live per-workspace chat) | ✔ — `WorkspaceRoute.tsx:57` already portals `<ChatSlot workspaceId>` into the `chat` tab via the `chatSlot` seam (`WorkspaceDesktopApp.tsx:112,885`) | reuse as-is |
|
||||
| **Memory** | `MemoryCenterTab` | ✔ — already embedded `WorkspaceDesktopApp.tsx:937-941` (`mind="workspace"`, `consumeDeepLinks={false}`) | reuse as-is (✅ G16) |
|
||||
| **Artifacts** | `ArtifactsRoute`/Artifact Center | ✔ route exists (`routes/index.ts:36`); workspace tab currently inlines a file-card grid (`:914-932`) | embed the Artifact Center component, or keep the file grid until the real artifact entity lands (§2 gap) |
|
||||
| **Files** | `FilesRoute` | ✔ route exists (`routes/index.ts:37`) | NEW tab — embed the Files surface scoped to the workspace; data via `getWorkspaceFiles` (already used) |
|
||||
| **Team** | `TeamRoute` | ✔ route exists (`routes/index.ts:48`) | NEW tab — embed Team; or a workspace-scoped panel. NB roster is global today (§2) |
|
||||
|
||||
**Drop from the bar** (no longer top-level per design): **Research** (placeholder stub
|
||||
only `:906-912`), **Tasks** (still a real `<TasksTab>` `:904` — relocate to the
|
||||
Overview "Up next" card + keep reachable, don't delete the component), **Timeline**
|
||||
(stub `:943-948` — lives at `/settings/timeline` per `routes/index.ts:29`), **Settings**
|
||||
(stub `:951-957` — lives at `/settings`). Update `WS_TABS` in `WorkspaceRoute.tsx:20-22`
|
||||
and the `WorkspaceTabId` union to the 6-tab set; unknown `:tab?` already falls back to
|
||||
overview (`WorkspaceRoute.tsx:43-45`), so stale `/workspaces/:id/tasks` links degrade
|
||||
gracefully.
|
||||
|
||||
**Reuse without change:**
|
||||
- **Warm tokens already shipped** (PR1): `apps/web/src/index.css` defines `--honey`,
|
||||
`--honey-wash`, `--honey-line`, `--honey-bright/-deep`, `--intel`, `--work`,
|
||||
`--healthy`, `--bg`, `--surface*`, `--text*` (verified `:149-167`) + the shadcn HSL
|
||||
core derives from them (`--primary` = honey `:29`). The Overview can use these
|
||||
directly. `apps/web/src/waggle-theme.css` is the companion sheet.
|
||||
- **Helpers in-file:** `initialsOf` (`:128-133`, for hex avatar + team), `relativeTime`
|
||||
(`:135-147`, for "updated Xago" + artifact times), `normalizeArtifacts` (`:977-998`),
|
||||
`humanizeActivitySummary` (`lib/activity-labels`).
|
||||
- **Hex motif:** README §7 `.hex { clip-path: polygon(...) }` — add a Tailwind/utility
|
||||
class for the 46px header avatar + the 30px fact check-tiles + 32px ext tiles.
|
||||
- **Data plumbing:** the existing `useEffect` load (`:613-679`) already fetches context/
|
||||
state/activity/members/files — **no new adapter calls needed** for Overview except
|
||||
optionally projecting `source` onto memory rows (server change, §2/G14) and a per-
|
||||
workspace artifacts/sources count (G2/G5).
|
||||
- **`WorkspaceBriefing.tsx`** importance pills + "I Remember"/"Recent Decisions" copy
|
||||
(`:181-199,164-178`) are a good reference for the "What Waggle knows" fact rows, but
|
||||
the briefing itself stays the chat empty-state — don't fold it into the shell.
|
||||
|
||||
**Build new:** the 2-col Overview grid + `FactRow` (hex check tile + provenance),
|
||||
`ArtRow` (ext tile + provenance + time), `StatusCard`, `UpNextCard`, `TeamCard`, the
|
||||
46px hex header avatar + breadcrumb + meta row, and the `Memory`/`Continue →` header
|
||||
buttons. Fold the current right `aside` content into the grid's right column (G13).
|
||||
301
docs/redesign-warm-hive/pr35-recon/01-frame-source-server.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# PR3.5 Recon — `frame.source` server read/write path (1-field projection)
|
||||
|
||||
**Goal:** add `source` to the `recentMemories` + `recentDecisions` items returned by the
|
||||
workspace-state / workspace-context API so the FE can render a ⬡ provenance pill on Chat +
|
||||
Workspace. This doc maps the exact write site, the exact read/SELECT/projection sites, the
|
||||
type-contract change, and the column's value vocabulary.
|
||||
|
||||
Repo root: `D:/Projects/waggle-os`. All line numbers verified 2026-06-16.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR — what to change
|
||||
|
||||
The ⬡ provenance pill on the **Workspace Briefing** (Chat + Workspace Overview both render
|
||||
`<WorkspaceBriefing>` fed by `GET /api/workspaces/:id/context`) is unlocked by **one route +
|
||||
one type change**:
|
||||
|
||||
1. `packages/server/src/local/routes/workspaces.ts` — add `source` to the two SELECTs (lines
|
||||
**393-400** and **409-417**) and to their two `.map()` projections (lines **402-406** and
|
||||
**419-429**).
|
||||
2. `apps/web/src/lib/types.ts` — widen `WorkspaceContext.recentMemories` / `recentDecisions`
|
||||
item shapes (lines **248-249**) with an optional `source?: string`.
|
||||
|
||||
That is the minimum. There is a SECOND, parallel context builder in
|
||||
`packages/server/src/local/routes/workspace-context.ts` (the `WorkspaceNowBlock` /
|
||||
`buildWorkspaceState` path) that feeds the **system prompt**, NOT the FE pill — see §4 for why
|
||||
it is out of scope, and the one ambiguity it raises.
|
||||
|
||||
---
|
||||
|
||||
## 1. WRITE path — where `frame.source` is set
|
||||
|
||||
### 1a. Schema DDL (the column itself)
|
||||
|
||||
`packages/hive-mind-core/src/mind/schema.ts:56-57`
|
||||
|
||||
```sql
|
||||
source TEXT NOT NULL DEFAULT 'user_stated'
|
||||
CHECK (source IN ('user_stated', 'tool_verified', 'agent_inferred', 'import', 'system')),
|
||||
```
|
||||
|
||||
Column lives on `memory_frames` (DDL `CREATE TABLE ... memory_frames` at `schema.ts:47`).
|
||||
Confirmed: the column name **is `source`** (not `frame_source` / `provenance`).
|
||||
|
||||
### 1b. Where rows are written with an explicit `source`
|
||||
|
||||
- **Template starter seeding** — `packages/server/src/local/routes/workspaces.ts:290-291`:
|
||||
|
||||
```sql
|
||||
INSERT INTO memory_frames (frame_type, gop_id, t, content, importance, source)
|
||||
VALUES ('I', ?, ?, ?, 'normal', 'system')
|
||||
```
|
||||
|
||||
i.e. workspace template starter memories are written with `source = 'system'`.
|
||||
|
||||
- **General frame writes** go through `FrameStore` in
|
||||
`packages/hive-mind-core/src/mind/frames.ts` — `createIFrame` /
|
||||
`createPFrame` (`frames.ts:74,119`) default `source: FrameSource = 'user_stated'`.
|
||||
- **MCP `save_memory`** maps an incoming `source` arg, defaulting to `'agent_inferred'`
|
||||
(`packages/memory-mcp/src/tools/memory.ts:35`,
|
||||
`packages/hive-mind-mcp-server/src/tools/memory.ts:34`).
|
||||
|
||||
So in practice persisted rows carry one of the five DB-CHECK values (see §3).
|
||||
|
||||
---
|
||||
|
||||
## 2. READ path — the SELECT + projection that feeds the FE (THE CHANGE SITE)
|
||||
|
||||
**Route:** `GET /api/workspaces/:id/context` —
|
||||
`packages/server/src/local/routes/workspaces.ts:364` (handler).
|
||||
This is the response consumed by `<WorkspaceBriefing>` (apps/web), which renders both the
|
||||
"Key memories" list and the "Recent decisions" list on Chat + Workspace.
|
||||
|
||||
### 2a. `recentMemories` — SELECT at `workspaces.ts:393-400`, projection at `402-406`
|
||||
|
||||
**BEFORE** (SELECT, lines 393-400):
|
||||
|
||||
```ts
|
||||
const frames = raw.prepare(
|
||||
`SELECT content, importance, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
ORDER BY CASE importance
|
||||
WHEN 'critical' THEN 1 WHEN 'important' THEN 2
|
||||
WHEN 'normal' THEN 3 ELSE 4 END,
|
||||
id DESC LIMIT 8`
|
||||
).all() as Array<{ content: string; importance: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
const frames = raw.prepare(
|
||||
`SELECT content, importance, source, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
ORDER BY CASE importance
|
||||
WHEN 'critical' THEN 1 WHEN 'important' THEN 2
|
||||
WHEN 'normal' THEN 3 ELSE 4 END,
|
||||
id DESC LIMIT 8`
|
||||
).all() as Array<{ content: string; importance: string; source: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**BEFORE** (projection, lines 402-406):
|
||||
|
||||
```ts
|
||||
recentMemories = frames.map(f => ({
|
||||
content: f.content.slice(0, 200),
|
||||
importance: f.importance,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
}));
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
recentMemories = frames.map(f => ({
|
||||
content: f.content.slice(0, 200),
|
||||
importance: f.importance,
|
||||
source: f.source,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
}));
|
||||
```
|
||||
|
||||
> The local `let recentMemories` is declared at `workspaces.ts:376` as
|
||||
> `Array<{ content: string; importance: string; date: string }>` — add `source: string;`
|
||||
> there too (or the `.map()` widening will type-error against the narrower local).
|
||||
|
||||
### 2b. `recentDecisions` — SELECT at `workspaces.ts:409-417`, projection at `419-429`
|
||||
|
||||
**BEFORE** (SELECT, lines 409-417):
|
||||
|
||||
```ts
|
||||
const decisionFrames = raw.prepare(
|
||||
`SELECT content, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
AND (content LIKE 'Decision%' OR content LIKE '%decided%'
|
||||
OR content LIKE '%decision made%' OR content LIKE '%chose %'
|
||||
OR content LIKE '%selected %' OR content LIKE '%agreed %'
|
||||
OR importance = 'critical')
|
||||
ORDER BY id DESC LIMIT 5`
|
||||
).all() as Array<{ content: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
const decisionFrames = raw.prepare(
|
||||
`SELECT content, source, created_at FROM memory_frames
|
||||
WHERE importance != 'deprecated' AND importance != 'temporary'
|
||||
AND (content LIKE 'Decision%' OR content LIKE '%decided%'
|
||||
OR content LIKE '%decision made%' OR content LIKE '%chose %'
|
||||
OR content LIKE '%selected %' OR content LIKE '%agreed %'
|
||||
OR importance = 'critical')
|
||||
ORDER BY id DESC LIMIT 5`
|
||||
).all() as Array<{ content: string; source: string; created_at: string }>;
|
||||
```
|
||||
|
||||
**BEFORE** (projection, lines 419-429):
|
||||
|
||||
```ts
|
||||
recentDecisions = decisionFrames.map(f => {
|
||||
const firstLine = f.content.split('\n')[0];
|
||||
const sentenceMatch = firstLine.match(/^(.+?\.\s)(?=[A-Z])/);
|
||||
const text = sentenceMatch
|
||||
? sentenceMatch[1].trim()
|
||||
: (firstLine.length > 150 ? firstLine.slice(0, 147) + '...' : firstLine);
|
||||
return {
|
||||
content: text.replace(/\.\s*$/, ''),
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**AFTER** — add `source: f.source,` to the returned object:
|
||||
|
||||
```ts
|
||||
return {
|
||||
content: text.replace(/\.\s*$/, ''),
|
||||
source: f.source,
|
||||
date: f.created_at?.slice(0, 10) ?? 'unknown',
|
||||
};
|
||||
```
|
||||
|
||||
> Same as above: the local `let recentDecisions` declared at `workspaces.ts:377` as
|
||||
> `Array<{ content: string; date: string }>` must gain `source: string;`.
|
||||
|
||||
### Response shape (unchanged structurally)
|
||||
|
||||
The handler returns an object whose `recentMemories` / `recentDecisions` fields are these two
|
||||
arrays (assembled further down in the same handler, returned as part of the workspace-context
|
||||
body). Adding `source` is purely additive — no existing consumer breaks.
|
||||
|
||||
---
|
||||
|
||||
## 3. TYPE-CONTRACT change
|
||||
|
||||
**File:** `apps/web/src/lib/types.ts` — interface `WorkspaceContext`, lines **248-249**:
|
||||
|
||||
**BEFORE**:
|
||||
|
||||
```ts
|
||||
recentDecisions?: Array<{ content: string; date: string }>;
|
||||
recentMemories?: Array<{ content: string; importance: string; date: string }>;
|
||||
```
|
||||
|
||||
**AFTER**:
|
||||
|
||||
```ts
|
||||
recentDecisions?: Array<{ content: string; source?: string; date: string }>;
|
||||
recentMemories?: Array<{ content: string; importance: string; source?: string; date: string }>;
|
||||
```
|
||||
|
||||
- Keep `source` **optional** (`source?:`) so older sidecars (pre-PR3.5) that don't emit the
|
||||
field still typecheck and the FE degrades gracefully (no pill when absent).
|
||||
- This is the ONLY type file that needs to change for the pill. `WorkspaceContext` is the FE
|
||||
mirror; there is no separate `@waggle/shared` interface for these inline item shapes (they're
|
||||
declared anonymously inline in both `workspaces.ts` and `types.ts`).
|
||||
- The FE consumer is `apps/web/src/components/os/WorkspaceBriefing.tsx` — `recentMemories`
|
||||
rendered at lines **186-197** (importance badge at 189-193, add the ⬡ pill alongside it),
|
||||
`recentDecisions` rendered at lines **164-178**. No type change needed in the component; it
|
||||
reads `m.content` / `m.importance` today and would add `m.source`.
|
||||
|
||||
> Note: the richer `@waggle/shared` `Memory` interface (`packages/shared/src/types.ts:517`,
|
||||
> field `source: string` at `:526`, doc-comment "maps from `memory_frames.source`") already
|
||||
> models provenance — but that's the Memory Center view-model, NOT the workspace-context item
|
||||
> shape. Do not route the pill through `Memory`; the context items are their own inline type.
|
||||
|
||||
---
|
||||
|
||||
## 4. The PARALLEL builder (workspace-context.ts) — out of scope, but flagged
|
||||
|
||||
`packages/server/src/local/routes/workspace-context.ts` builds `WorkspaceNowBlock`
|
||||
(`recentDecisions: string[]`, type at `:14-21`) for **system-prompt injection**, via
|
||||
`buildWorkspaceState()` in `packages/server/src/local/workspace-state.ts:234`. Its decision
|
||||
SELECT is `workspace-state.ts:86-94` (`SELECT id, content, created_at`) and the legacy inline
|
||||
one is `workspace-context.ts:362` (`SELECT content`). Its items are typed `StateItem`
|
||||
(`workspace-state.ts:30-36`) whose `source` field is a `StateSource =
|
||||
'memory'|'session'|'awareness'` — that is a DIFFERENT `source` axis (where in the substrate the
|
||||
item came from), **not** the `frame.source` provenance class.
|
||||
|
||||
**Decision:** the FE pill is fed by §2 (`workspaces.ts` `/context` route → `WorkspaceContext`
|
||||
→ `WorkspaceBriefing`), so PR3.5's 1-field projection only needs §2 + §3. The
|
||||
`workspace-state.ts` / `WorkspaceNowBlock` path does not surface to the pill and can be left
|
||||
untouched. If a future task wants frame-provenance in the system prompt too, that's a separate,
|
||||
larger change (it would collide with the existing `StateItem.source` name).
|
||||
|
||||
`home.ts` (`GET /api/home/briefing`) consumes `buildWorkspaceState().recentDecisions[0].content`
|
||||
(`home.ts:314-315`) only — it reads `.content`, never `.source`, so it is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## 5. SOURCE-column value vocabulary
|
||||
|
||||
**DB CHECK constraint (authoritative for persisted rows)** —
|
||||
`schema.ts:57`:
|
||||
|
||||
| value | meaning |
|
||||
|---|---|
|
||||
| `user_stated` | user said it directly (FrameStore default) |
|
||||
| `tool_verified` | confirmed by a tool execution |
|
||||
| `agent_inferred` | agent inferred it (MCP `save_memory` default) |
|
||||
| `import` | brought in via harvest/import |
|
||||
| `system` | system-seeded (e.g. workspace template starter memory — `workspaces.ts:291`) |
|
||||
|
||||
**TS `FrameSource` union is WIDER than the DB CHECK** —
|
||||
`packages/hive-mind-core/src/mind/frames.ts:25`:
|
||||
|
||||
```ts
|
||||
export type FrameSource = 'user_stated' | 'tool_verified' | 'agent_inferred'
|
||||
| 'import' | 'system' | 'personal' | 'workspace' | 'team_sync';
|
||||
```
|
||||
|
||||
The extra three (`personal` / `workspace` / `team_sync`) are application-level labels that the
|
||||
DB CHECK does **not** allow, so a constrained INSERT with one of them would fail — **persisted
|
||||
rows can only ever hold the 5 CHECK values** (documented drift:
|
||||
`docs/backend-map/sections/02a-data-model-memory.md:109` and `.../05b-subsystem-memory.md:318`).
|
||||
|
||||
**FE pill mapping guidance:** the pill should map the 5 real values to friendly labels/icons
|
||||
(e.g. `user_stated`→"you", `tool_verified`→"verified", `agent_inferred`→"agent",
|
||||
`import`→"imported", `system`→"system"). Treat anything else as a graceful fallback. Because
|
||||
the projection returns the raw string, the FE owns the label map (do not hardcode it in the
|
||||
route).
|
||||
|
||||
---
|
||||
|
||||
## 6. Honest ambiguities / risks
|
||||
|
||||
1. **Two SELECTs, one new column each** — straightforward, additive. The only typecheck trap is
|
||||
the narrower `let recentMemories` / `let recentDecisions` declarations at `workspaces.ts:376-377`;
|
||||
widen those too or `tsc` fails. (Reminder per CLAUDE.md §2: `npm run build` typechecks
|
||||
`apps/web` only — run `npx tsc --noEmit --project packages/server/tsconfig.json` to catch a
|
||||
server-route type error, since the sidecar runs via `tsx` transpile-only.)
|
||||
2. **The "source" name is overloaded** — `frame.source` (provenance: 5 CHECK values) vs
|
||||
`StateItem.source` (`memory|session|awareness`, where-it-came-from) vs `WorkspaceContext`
|
||||
inline items (currently no `source`). Make sure the pill consumes the PROVENANCE one from §2,
|
||||
not the `StateItem` one.
|
||||
3. **Decision frames may carry any of the 5 sources** — the decision SELECT (`workspaces.ts:409`)
|
||||
filters by `content LIKE` / `importance='critical'`, not by `source`, so a "decision" can be
|
||||
`agent_inferred` or `user_stated` etc. That's fine for a pill; just don't assume decisions
|
||||
are always `system`.
|
||||
4. **No migration / no DDL change needed** — `source` already exists on every DB (NOT NULL
|
||||
DEFAULT). The projection is read-only over an existing column. Zero data-backfill risk.
|
||||
215
docs/redesign-warm-hive/pr35-recon/02-sse-step-path.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# PR3.5 Recon — SSE/Chat "step" event path & provenance attach point
|
||||
|
||||
**Question:** Can a `source`/provenance field attach to streamed agent "step" events
|
||||
(to power the `⬡ source · when` ProvenanceLine pill that PR3 wired date-only)?
|
||||
|
||||
**Verdict: NEEDS-WIRING.** A streamed `step` event today carries *only* `{ content: string }` —
|
||||
no frame id, no `source`, no provenance of any kind. The frame `source` *does* exist in the
|
||||
substrate (`MemoryFrame.source: FrameSource`), but it is flattened to formatted text strings
|
||||
before it ever reaches the SSE layer. Attaching `source` to a step is a real (but small &
|
||||
well-scoped) plumbing job, not a trivial projection. The cleanest path is **not** to enrich the
|
||||
generic `step` event — it's to enrich the `tool_result` event for the `auto_recall` (memory) tool,
|
||||
which is the only step type that has a real provenance signal.
|
||||
|
||||
---
|
||||
|
||||
## 1. Server — where steps are produced & streamed
|
||||
|
||||
**Route:** `packages/server/src/local/routes/chat.ts` — `POST /api/chat`, SSE via `reply.hijack()`.
|
||||
|
||||
**SSE writer (the only emit helper):** `chat.ts:475`
|
||||
```ts
|
||||
const sendEvent = (event: string, data: unknown) => {
|
||||
raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
```
|
||||
|
||||
### The `step` payload shape (server)
|
||||
Every `step` event the server emits has the **same minimal shape**: `{ content: string }`. There is
|
||||
no other field. Representative emit sites:
|
||||
|
||||
- `chat.ts:761` — `sendEvent('step', { content: 'Recalling relevant memories...' })`
|
||||
- `chat.ts:780` — `sendEvent('step', { content: 'Recalled N relevant memories.' })`
|
||||
- `chat.ts:1170` — `sendEvent('step', { content: stepText })` where `stepText = describeToolUse(name, input)`
|
||||
(this is the **per-tool-call** step, fired from the agent-loop `onToolUse` callback — the main step source)
|
||||
- plus budget/approval/compression/GEPA steps at `:740 :754 :771 :904 :921 :929 :1023 :1027 :1108 :1352 :1382 :1389 :1439 :1458` — all `{ content }` only.
|
||||
|
||||
### The agent-loop emission (where tool steps originate)
|
||||
**`packages/agent/src/agent-loop.ts`** does **not** emit `step` events itself. It exposes typed
|
||||
callbacks (`AgentLoopConfig`, `agent-loop.ts:37-39`):
|
||||
```ts
|
||||
onToken?: (token: string) => void;
|
||||
onToolUse?: (name: string, input: Record<string, unknown>) => void;
|
||||
onToolResult?: (name: string, input: Record<string, unknown>, result: string) => void;
|
||||
```
|
||||
The route wires these to SSE in the runner config (`chat.ts:1167-1185` / `:1186-1220`):
|
||||
```ts
|
||||
onToolUse: (name, input) => {
|
||||
const stepText = describeToolUse(name, input);
|
||||
sendEvent('step', { content: stepText }); // ← the step
|
||||
sendEvent('tool', { name, input }); // ← raw tool event (has name+input)
|
||||
...
|
||||
},
|
||||
onToolResult: (name, input, result) => {
|
||||
...
|
||||
sendEvent('tool_result', { name, result, duration, isError }); // ← name+result+duration
|
||||
...
|
||||
},
|
||||
```
|
||||
**Key fact:** the loop's callbacks expose `name`, `input`, `result` — but **no frame, no `source`,
|
||||
no provenance**. The agent loop has no concept of which memory frame a step touched.
|
||||
|
||||
### The related `tool_result` shape (server)
|
||||
`chat.ts:1200` — `{ name, result, duration, isError }`. For the memory tool specifically:
|
||||
`chat.ts:781` — `sendEvent('tool_result', { name: 'auto_recall', result: resultText, duration, isError })`
|
||||
where `resultText` is **already-rendered text** (`chat.ts:777-779`):
|
||||
```ts
|
||||
const snippets = (recall.recalled ?? []).slice(0, 3);
|
||||
const snippetText = snippets.map(s => ` - ${s}`).join('\n');
|
||||
const resultText = `${recall.count} memories recalled:\n${snippetText}`;
|
||||
```
|
||||
|
||||
### Why the provenance is lost: `recallMemory()` returns text, not frames
|
||||
`packages/agent/src/orchestrator.ts:453-457`:
|
||||
```ts
|
||||
async recallMemory(...): Promise<{ text: string; count: number; recalled?: string[] }>
|
||||
```
|
||||
`recalled` is a `string[]` of **formatted snippets** — the frame objects (which *do* carry
|
||||
`source`) are collapsed to display strings inside `recallMemory` before returning. The real
|
||||
provenance lives one layer deeper: `packages/hive-mind-core/src/mind/frames.ts:35` —
|
||||
`MemoryFrame.source: FrameSource` (e.g. `user_stated`, harvest adapters: chatgpt/claude/etc.),
|
||||
plus `sourceUrl`/`sourceId` extras. So **the data exists in the DB, it is just not projected up
|
||||
through `recallMemory` → SSE.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend — SSE consumer → activity stream blocks
|
||||
|
||||
### SSE parse + event-type mapping
|
||||
**`apps/web/src/lib/adapter.ts:692-738`** (`streamChat` async generator) parses the raw SSE
|
||||
text and maps event names to `StreamEvent.type`:
|
||||
- `adapter.ts:729` — `else if (type === 'step') type = 'step';`
|
||||
- yields `{ type, data } as StreamEvent` (`adapter.ts:732`) — `data` is the parsed `{ content }`.
|
||||
|
||||
### `StreamEvent` type
|
||||
**`apps/web/src/lib/types.ts:616-619`**:
|
||||
```ts
|
||||
export interface StreamEvent {
|
||||
type: 'token' | 'step' | 'tool_start' | 'tool_end' | 'done' | 'error' | 'approval_request' | 'approval_required' | 'model_switch' | 'notification';
|
||||
data: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### Step → block reduction
|
||||
**`apps/web/src/hooks/useChat.ts:152-165`** turns a `step` event into a `StepContentBlock`:
|
||||
```ts
|
||||
case 'step': {
|
||||
const description = typeof data === 'string' ? data : (data?.content as string ?? '');
|
||||
if (description) {
|
||||
// mark prior running steps done
|
||||
blocks.push({ type: 'step', blockId: nextBlockId('step'), description, status: 'running' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### `StepContentBlock` type (client) — **the field set to extend**
|
||||
**`apps/web/src/lib/types.ts:474-479`**:
|
||||
```ts
|
||||
export interface StepContentBlock {
|
||||
type: 'step';
|
||||
blockId: string;
|
||||
description: string;
|
||||
status: 'running' | 'done';
|
||||
}
|
||||
```
|
||||
No `source`/`provenance` field. This is the type that would gain an optional
|
||||
`provenance?: { source: string; when?: string }`.
|
||||
|
||||
### Where provenance is (not yet) shown — PR3's date-only affordance
|
||||
**`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:28-43`** groups a consecutive run
|
||||
of step blocks into one `ActivityStream` card. PR3 already left the hook here and an honest comment
|
||||
(`BlockRenderer.tsx:24-26`):
|
||||
```ts
|
||||
// Provenance pills are intentionally omitted: the SSE
|
||||
// `step` payload carries no structured source field today (recon chat.md §4) —
|
||||
// we render the affordance, never fabricated provenance.
|
||||
```
|
||||
And the map drops provenance (`BlockRenderer.tsx:30-33`):
|
||||
```ts
|
||||
const activitySteps: ActivityStep[] = steps.map(s => ({
|
||||
tone: s.status === 'running' ? 'honey' : 'intel',
|
||||
text: s.description, // ← no provenance projected
|
||||
}));
|
||||
```
|
||||
|
||||
The rendering target already exists and is wired:
|
||||
- **`ActivityStream`** (`components/os/warm/ActivityStream.tsx:8-13`) — `ActivityStep.provenance?: { source; when?; onClick? }`, rendered at `:67-71` **only when present**.
|
||||
- **`ProvenanceLine`** (`components/os/warm/ProvenanceLine.tsx:19-32`) — the `⬡ source · when` pill.
|
||||
|
||||
So the **client consumer is provenance-ready**; it is waiting for the server to deliver a `source`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Honest verdict — trivial vs needs-wiring
|
||||
|
||||
**NEEDS-WIRING (small, scoped). NOT trivially projectable.**
|
||||
|
||||
- A generic `step` (e.g. "Drafting the document", "Budget limit reached", a `bash` tool call) has
|
||||
**no provenance at all** and never will — those steps are not memory reads. Stamping them with a
|
||||
`source` would be fabrication (exactly what PR3's comment refuses). So a blanket "add source to
|
||||
every step" is wrong on the merits.
|
||||
- The **one** step with a genuine provenance signal is the **memory recall** (`auto_recall`). Its
|
||||
source is real (`MemoryFrame.source`) but is destroyed by `recallMemory()` returning `string[]`
|
||||
text instead of frame metadata.
|
||||
|
||||
### The precise new wiring needed
|
||||
1. **Substrate → orchestrator (the load-bearing change):** widen `recallMemory()`'s return in
|
||||
`packages/agent/src/orchestrator.ts:453-457` to carry per-snippet provenance, e.g. add
|
||||
`recalledFrames?: Array<{ text: string; source: FrameSource; sourceUrl?: string; when?: string }>`
|
||||
alongside the existing `recalled: string[]`. The frame objects with `.source` are already in hand
|
||||
inside `recallMemory` — this is a "stop flattening it" change, not a new query. (This is the
|
||||
*same* 1-field server projection the S2 handoff named: *"the `frame.source` 1-field server
|
||||
projection (unlocks the ⬡ provenance pill on Chat+Workspace)"*.)
|
||||
2. **Server SSE:** in `chat.ts` around `:780-781`, emit the source on the memory step/tool_result —
|
||||
either add `source`/`provenance` to the existing `tool_result` (`{ name:'auto_recall', ... }`) or
|
||||
to the `step` payload for that one event: `sendEvent('step', { content, provenance: { source, when } })`.
|
||||
(`{ content }` → `{ content, provenance? }` is additive and backward-compatible.)
|
||||
3. **Client types:** add optional `provenance?: { source: string; when?: string }` to
|
||||
`StepContentBlock` (`types.ts:474`) and to the `StreamEvent` `data` handling.
|
||||
4. **Client reducer:** in `useChat.ts:152` carry `data.provenance` onto the pushed step block.
|
||||
5. **Client render:** in `BlockRenderer.tsx:30-33` project `s.provenance` into `ActivityStep` (the
|
||||
`ActivityStream`/`ProvenanceLine` rendering path already exists and gates on presence).
|
||||
|
||||
**Effort estimate:** ~5 small edits across 4 files (orchestrator return-shape widen is the only
|
||||
non-trivial one; everything else is a 1-field pass-through). No new DB columns, no new query — the
|
||||
`source` already exists at `frames.ts:35`. Lowest-risk slice = wire it on `auto_recall` only
|
||||
(memory steps), leave all other steps provenance-less by design.
|
||||
|
||||
### Scope call for PR3.5
|
||||
**In-scope and the right size for PR3.5** *iff* paired with the `recallMemory` return-shape widen.
|
||||
If PR3.5 wants to stay frontend-only, then provenance on steps is **out-of-scope** (the data is not
|
||||
on the wire) — and the honest move is to keep PR3's "render the affordance, never fabricated
|
||||
provenance" stance until the server projection lands.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — file:line index
|
||||
|
||||
| Concern | File:line |
|
||||
|---|---|
|
||||
| SSE writer | `packages/server/src/local/routes/chat.ts:475` |
|
||||
| step payload (memory) | `chat.ts:761`, `:780` |
|
||||
| step payload (per tool call) | `chat.ts:1170` (via `describeToolUse`) |
|
||||
| tool_result payload | `chat.ts:1200`; memory variant `chat.ts:781` |
|
||||
| recall snippet build (text flatten) | `chat.ts:777-779` |
|
||||
| agent-loop callbacks (no source) | `packages/agent/src/agent-loop.ts:37-39`, wired `chat.ts:1167-1220` |
|
||||
| recallMemory return shape | `packages/agent/src/orchestrator.ts:453-457` |
|
||||
| frame `source` field (real provenance) | `packages/hive-mind-core/src/mind/frames.ts:35` |
|
||||
| SSE parse + map | `apps/web/src/lib/adapter.ts:717-732` |
|
||||
| StreamEvent type | `apps/web/src/lib/types.ts:616-619` |
|
||||
| step → block reducer | `apps/web/src/hooks/useChat.ts:152-165` |
|
||||
| StepContentBlock type | `apps/web/src/lib/types.ts:474-479` |
|
||||
| activity grouping + omitted-provenance comment | `apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:24-43` |
|
||||
| ActivityStream (provenance-ready) | `apps/web/src/components/os/warm/ActivityStream.tsx:8-13, 67-71` |
|
||||
| ProvenanceLine pill | `apps/web/src/components/os/warm/ProvenanceLine.tsx:19-32` |
|
||||
155
docs/redesign-warm-hive/pr35-recon/03-pr3-hooks-primitives.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# PR3.5 Recon — What PR3 actually left behind (hooks + primitives)
|
||||
|
||||
> Audit, not trust. Every row below was verified against source on branch
|
||||
> `feature/warm-hive-pr3` @ `dac7b696`. Where the handoff was imprecise it is
|
||||
> called out. Repo root: `D:/Projects/waggle-os`.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — claimed primitives: exist vs must-build
|
||||
|
||||
**The three "Memory-Trust primitives" the handoff named — `ConfidenceBadge`,
|
||||
`EvidenceChip`, `DetailDrawer` — ALL EXIST and are already in production use.**
|
||||
The handoff's only error was the *location*: they are NOT in `os/warm/`, they live
|
||||
in `components/ui/` and predate PR3 (created in the earlier UX-refactor arc, commit
|
||||
`afe96355` "Phase 2B-FE.1 — Memory Center DS foundation"). PR3 added the warm
|
||||
provenance layer (`ProvenanceLine`, `ActivityStream`) on top.
|
||||
|
||||
So PR3.5 **reuses**, does not build-from-scratch, the trust primitives. What is
|
||||
genuinely **missing / static** is the *wiring*: the Workspace "What Waggle knows"
|
||||
fact rows and the Chat activity step rows are visual-only (no `onClick`, no
|
||||
provenance plumbed), and the chat `StepContentBlock` type has no `source` field —
|
||||
that is the real PR3.5 work (the "frame.source 1-field server projection" keystone).
|
||||
|
||||
---
|
||||
|
||||
## TRUTH TABLE — claim → real state → file:line
|
||||
|
||||
| # | Handoff claim | Real state | Evidence (file:line) |
|
||||
|---|---|---|---|
|
||||
| 1 | "ConfidenceBadge / EvidenceChip / DetailDrawer primitives **ready** [implied: in `warm/`]" | **TRUE but mislocated.** All three exist in `components/ui/`, NOT `os/warm/`. Pre-date PR3 (commit `afe96355`). Already consumed by Memory Center. Do NOT rebuild. | `apps/web/src/components/ui/confidence-badge.tsx:22` (export `ConfidenceBadge`)<br>`apps/web/src/components/ui/evidence-chip.tsx:16` (export `EvidenceChip`)<br>`apps/web/src/components/ui/detail-drawer.tsx:22` (export `DetailDrawer`)<br>Bonus: `apps/web/src/components/ui/evidence-panel.tsx` (`EvidencePanel`) also exists |
|
||||
| 1b | (warm/ dir listing) | The PR3 `warm/` set is 14 atoms; the trust primitives are NOT among them. warm/ has the provenance *renderer* (`ProvenanceLine`) + the chat *container* (`ActivityStream`). | `apps/web/src/components/os/warm/index.ts:7-20` |
|
||||
| 2a | "clickable fact rows" (Workspace "What Waggle knows", PR3 Phase C2) | **FALSE — STATIC.** The `<li>` rows render `HexCheckTile + text + when`. No `onClick`, no anchor, no role=button, no cursor affordance. Pure display. | `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx:160-168` (the `facts.map` `<li>` has no handler). Header comment at `:146-149` explicitly flags `frame.source` is not yet projected onto these rows ("the PR3.5 keystone"). |
|
||||
| 2b | "clickable step rows" (Chat activity steps) | **FALSE — STATIC.** `ActivityStream` renders each step as a `<li>` (`DotLive + text + optional ProvenanceLine`). The step `<li>` itself has no `onClick`. Provenance is only rendered `if (s.provenance)` — and the Chat caller never supplies it. | `apps/web/src/components/os/warm/ActivityStream.tsx:62-74` (step `<li>` static; provenance gated on `s.provenance`)<br>`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:30-33` (maps only `tone`+`text` — **omits `provenance` entirely**) |
|
||||
| 2c | (why chat steps can't show provenance yet) | **Root blocker.** `StepContentBlock` has no `source`/`provenance` field. So even if BlockRenderer wanted to pass provenance, the data isn't on the block. Same shape as the Workspace keystone — both need `frame.source` server projection. | `apps/web/src/lib/types.ts:474-479` (`StepContentBlock` = `{type,blockId,description,status}` — no source) |
|
||||
| 3 | "intact J08 banner" | **TRUE.** Renders on Home Cockpit; gated `needsReviewCount > 0`; `role="alert"`; deep-links to Memory Center via `waggle:open-app` with `filter:'unreviewed'`. Wiring intact end-to-end (route re-stash in `MemoryRoute.tsx`, consumed by `MemoryCenterTab`). | Render: `apps/web/src/components/os/apps/HomeCockpit.tsx:496-515`<br>Handler `openMemoryReview`: `:477-481`<br>Count source: `briefing.needsReviewCount` (`lib/types.ts:303`)<br>Consumer: `MemoryCenterTab.tsx:84` |
|
||||
| 4 | "ProvenanceLine.tsx" | **EXISTS** (PR3 Phase 0, commit `0b15b91f`). Thin recolor of `EvidenceChip` to the intel/violet semantic. Full API below. | `apps/web/src/components/os/warm/ProvenanceLine.tsx:19` |
|
||||
| 5 | "warm/index.ts + tones.ts" | **EXIST.** Full exports + tone vocab below. | `apps/web/src/components/os/warm/index.ts`, `tones.ts` |
|
||||
|
||||
---
|
||||
|
||||
## ProvenanceLine — full API (verified, `ProvenanceLine.tsx:4-32`)
|
||||
|
||||
```ts
|
||||
interface ProvenanceLineProps {
|
||||
source: string; // REQUIRED. e.g. "web · mem0.ai" or "Claude Code"
|
||||
when?: string; // optional relative time, e.g. "2h ago"
|
||||
onClick?: () => void; // PR3.5 trace hook — makes the pill clickable into memory detail
|
||||
className?: string;
|
||||
}
|
||||
export function ProvenanceLine({ source, when, onClick, className }): JSX.Element
|
||||
```
|
||||
|
||||
**Render:** builds `label = \`⬡ ${source}${when ? \` · ${when}\` : ''}\``, then
|
||||
delegates to `<EvidenceChip label title={label} onClick className=...>`. The chip is
|
||||
recolored via `font-mono text-[var(--intel)] border-[var(--intel-wash)] bg-transparent`.
|
||||
|
||||
- **Clickable behavior:** when `onClick` is supplied, `EvidenceChip` renders a real
|
||||
`<button type="button">` with `hover:bg-muted hover:text-foreground` (`evidence-chip.tsx:19-25`);
|
||||
without it, a static `<span>` (`:26-30`). So `ProvenanceLine` is click-ready **iff the
|
||||
caller passes `onClick`** — the primitive supports it; the call sites don't use it yet.
|
||||
- **`source` absent → date-only:** `source` is a **required** prop, so the pill cannot
|
||||
be rendered "date-only" via ProvenanceLine. The actual date-only fallback today is done
|
||||
by **not rendering ProvenanceLine at all** and showing a plain mono date instead — see
|
||||
the Workspace fact rows (`WorkspaceDesktopApp.tsx:165`, a bare
|
||||
`<div class="font-mono text-[10.5px]">{f.when}</div>`). The deliberate design note: PR3
|
||||
shows the REAL date only and **never fabricates a `source`** until `frame.source` is
|
||||
projected (`WorkspaceDesktopApp.tsx:146-149`). **PR3.5 implication:** to light up the
|
||||
⬡ pill, project `frame.source` server-side, then swap the bare date `<div>` for
|
||||
`<ProvenanceLine source={...} when={f.when} onClick={...} />`.
|
||||
|
||||
---
|
||||
|
||||
## Tone / token vocabulary (for new Memory-Trust primitives)
|
||||
|
||||
### `os/warm/tones.ts` — `WarmTone` union + maps (`tones.ts:7-36`)
|
||||
|
||||
```ts
|
||||
type WarmTone = 'work' | 'intel' | 'healthy' | 'attention' | 'risk' | 'honey' | 'neutral';
|
||||
|
||||
TONE_COLOR: Record<WarmTone, string> // foreground/dot color
|
||||
work→var(--work) intel→var(--intel) healthy→var(--healthy)
|
||||
attention→var(--attention) risk→var(--risk) honey→var(--honey)
|
||||
neutral→var(--text-muted)
|
||||
|
||||
TONE_WASH: Record<WarmTone, string> // tinted background
|
||||
work→var(--work-wash) intel→var(--intel-wash) healthy→var(--healthy-wash)
|
||||
attention→var(--honey-wash) risk→var(--risk-wash) honey→var(--honey-wash)
|
||||
neutral→var(--surface-3)
|
||||
```
|
||||
|
||||
> Note the asymmetry: `attention` foreground = `--attention`, but its wash = `--honey-wash`
|
||||
> (not `--attention-wash`). Match this when building a trust primitive on the attention tone.
|
||||
|
||||
### CSS tokens — defined in BOTH themes (`apps/web/src/index.css`)
|
||||
|
||||
| Token | Dark (`:162-168`) | Light (`:298-304`) | Semantic |
|
||||
|---|---|---|---|
|
||||
| `--intel` | `#b196dd` | `#7d57b8` | **violet — intelligence / memory / provenance** (the trust color) |
|
||||
| `--intel-wash` | `rgba(177,150,221,.12)` | `rgba(125,87,184,.10)` | violet wash |
|
||||
| `--work` / `--work-wash` | desaturated blue | — | task/work |
|
||||
| `--healthy` / `--healthy-wash` | desaturated green | — | OK/done |
|
||||
| `--attention` | (status block `:102+`) | — | needs-attention (uses `--honey-wash`) |
|
||||
| `--risk` / `--risk-wash` | — | — | blocked/conflict |
|
||||
| `--honey` / `--honey-wash` | — | — | brand accent |
|
||||
|
||||
The `confidence-badge` uses a *different* token family — Hive DS `--sem-*`
|
||||
(`--sem-healthy` / `--sem-attention` / `--sem-risk`, `confidence-badge.tsx:17-19`) — not
|
||||
the warm `--intel/--work/...`. **PR3.5 consistency call:** decide whether new
|
||||
Memory-Trust atoms align to the warm `--intel` family (provenance) or the `--sem-*`
|
||||
family (confidence bands). They currently coexist.
|
||||
|
||||
---
|
||||
|
||||
## warm/index.ts — full export list (`index.ts:7-20`)
|
||||
|
||||
```
|
||||
HexAvatar · SectionLabel · DotLive · ProvenanceLine · RunChip (+RunChipProps)
|
||||
IconTile · HexCheckTile · StreakChip · ModelPill · OvernightHero · AskBar
|
||||
ActivityStream (+ActivityStep) · InlineApprovalCard · TONE_COLOR · TONE_WASH (+WarmTone)
|
||||
```
|
||||
|
||||
14 atoms. **Not present (and not the warm dir's job per its header doc, `index.ts:3-6`):**
|
||||
ConfidenceBadge / EvidenceChip / DetailDrawer — "Generic, always-labeled primitives
|
||||
stay in `components/ui/`."
|
||||
|
||||
---
|
||||
|
||||
## Where the Memory surfaces live today (screen 19 target map)
|
||||
|
||||
| Surface | File | Notes for PR3.5 |
|
||||
|---|---|---|
|
||||
| **Memory Center (standalone screen, S04)** | `apps/web/src/components/os/apps/MemoryCenterApp.tsx` | The full screen. **Two-mind split** ("About you" = personal, "About this work" = workspace) + 7 views: `memories · timeline · graph · harvest · weaver · wiki · evolution` (`MEMORY_VIEWS`, `:36-38`). Controlled component (route owns mind+view via URL). This is what **screen 19 extends or replaces.** |
|
||||
| **Per-mind memory list (the reusable core)** | `apps/web/src/components/os/apps/memory/MemoryCenterTab.tsx` | Parameterized by `mind` + `workspaceId`. **Already consumes the trust primitives:** `DetailDrawer` (`:337`), `ConfidenceBadge` (`:342`), `EvidencePanel`+source/evidence (`:406`), `MemoryCard` grid (`:323-331`, each card `onClick={() => openDetail(m)}`). This is the proven pattern PR3.5 should match. |
|
||||
| **Workspace Memory tab** | `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx:701-705` | Tab `id:'memory'` (`TABS`, `:73`) embeds the SAME `<MemoryCenterTab mind="workspace" workspaceId consumeDeepLinks={false} />`. So the Workspace memory tab and the Memory Center already share one component — change once, both update. |
|
||||
| **Route wrapper** | `apps/web/src/routes/MemoryRoute.tsx` | `/memory` ≡ `/memory/personal`; J08 banner target; re-stashes `?filter=` deep link. |
|
||||
| **Memory card row** | `apps/web/src/components/os/apps/memory/MemoryCard.tsx` | Individual memory row component (grep-confirmed it references ConfidenceBadge family). |
|
||||
| **J08 needs-review banner** | `apps/web/src/components/os/apps/HomeCockpit.tsx:496-515` | Already wired; deep-links here. |
|
||||
| **Import reminder banner** | `apps/web/src/components/os/apps/memory/ImportReminderBanner.tsx` | Secondary banner on the Memory Center. |
|
||||
|
||||
---
|
||||
|
||||
## PR3.5 build implications (derived from the truth table)
|
||||
|
||||
1. **Reuse, don't rebuild** the trust primitives (`ui/confidence-badge`,
|
||||
`ui/evidence-chip`, `ui/detail-drawer`, `ui/evidence-panel`) — they exist and ship today.
|
||||
2. **The keystone is the data, not the UI:** project `frame.source` (+ `when`) server-side
|
||||
so the Workspace fact rows and Chat steps can carry real provenance.
|
||||
3. **Make the rows clickable** by (a) adding `onClick` to the Workspace fact `<li>`
|
||||
(`WorkspaceDesktopApp.tsx:160`) → open the memory detail drawer, (b) extending
|
||||
`StepContentBlock` (`types.ts:474`) with an optional `source` and threading it through
|
||||
`BlockRenderer.tsx:30-33` into `ActivityStep.provenance`.
|
||||
4. **`ProvenanceLine` already supports `onClick`** (→ `EvidenceChip` renders a `<button>`);
|
||||
the only gap is call sites passing the handler + a real `source`.
|
||||
5. **Token consistency decision** pending: warm `--intel` family (provenance) vs Hive DS
|
||||
`--sem-*` family (confidence). Both live; pick one story for screen 19.
|
||||
317
docs/redesign-warm-hive/pr35-recon/04-design-screen19.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Screen 19 — Memory Trust · Build-Ready Design Contract
|
||||
|
||||
> Source of truth: `docs/design_handoff_waggle_app/design-files/screens/memory-trust.html`
|
||||
> (authoritative mock, read in full), `screenshots/19-memory-trust.png` (rendered),
|
||||
> `DESIGN_POV.md` #1, `SCREENS.md` (no 01–18 entry — this screen is POV-driven, #19).
|
||||
> All copy below is **verbatim** from the mock. All class names / colors / spacing are
|
||||
> quoted from the HTML `<style>` block.
|
||||
|
||||
**DESIGN_POV #1 mandate (verbatim, `DESIGN_POV.md:12-34`):** "A persistent-memory
|
||||
product's #1 churn driver isn't *forgetting* — it's **remembering the wrong thing**…"
|
||||
The designed response is a *Memory Trust* layer with four primitives: **confidence +
|
||||
freshness** on every memory; **Forget** (real removal from recall) and **Correct**
|
||||
(inline; dependents re-checked); **Stale review** prompts; **"Why did you do that?"
|
||||
trace**. It is "also a moat… auditability, right-to-correct, EU AI Act alignment."
|
||||
POV §How-to-use: "wire it to the real memory store (confidence/freshness/forget/correct/
|
||||
trace are all backed by data the substrate already has or can derive)."
|
||||
|
||||
---
|
||||
|
||||
## 1. Header / Views (the segmented control)
|
||||
|
||||
The page is a single full-height column (`body { height:100vh; overflow:hidden;
|
||||
display:flex; flex-direction:column }`). A sticky control bar (`.controls`, blurred
|
||||
`backdrop-filter:blur(10px)`, `border-bottom:1px solid var(--line-soft)`) holds a
|
||||
**2-button segmented switch** that toggles between two full views.
|
||||
|
||||
| Element | Verbatim copy / spec |
|
||||
|---|---|
|
||||
| Eyebrow label (`.lab`, mono 10.5px, `--text-dim`, uppercase) | `Memory Trust · view` |
|
||||
| Segment button 1 (`.seg button.on` — **active by default**) | `Manage memory` |
|
||||
| Segment button 2 (`.seg button`) | `Why did you do that?` |
|
||||
| Active-view label (`.vlabel`, swaps on toggle) — Manage | `<b>Manage</b> — forget, correct, confirm; see confidence & freshness` |
|
||||
| Active-view label — Why | `<b>Why-trace</b> — every action explains itself` |
|
||||
| Right control (`.tbtn`) | Theme toggle `☾`/`☀` |
|
||||
|
||||
**Segment active state:** `.seg button.on { background:var(--honey); color:#1a1407; }`
|
||||
(honey fill + dark-ink text — the canonical "selected" treatment). Inactive buttons are
|
||||
`background:transparent; color:var(--text-muted)`. Segment container `.seg` is a
|
||||
`var(--surface-2)` pill with `border:1px solid var(--line-soft)`, `border-radius:10px`,
|
||||
3px inner padding.
|
||||
|
||||
**Two VIEW modes:**
|
||||
- **A — "Manage memory"** (`.view[data-view="manage"]`, on by default — this is what the
|
||||
screenshot shows): editorial hero → 4 stat cards → search+filters → memory rows → trust
|
||||
principle footnote.
|
||||
- **B — "Why did you do that?"** (`.view[data-view="why"]`): editorial hero → a single
|
||||
**trace card** (goal→recall→checks→action chain) → trust principle footnote. (See §6.)
|
||||
|
||||
Toggle JS: clicking a segment button sets `.on`, shows the matching `.view`, swaps the
|
||||
`.vlabel` HTML, and resets `.stage` scroll to 0. Stage content is centered in
|
||||
`.wrap { max-width:920px; margin:0 auto; padding:30px 32px 70px }`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Editorial Hero
|
||||
|
||||
### View A — Manage (`.head`, margin-bottom 22px)
|
||||
- **Eyebrow** (`.eyebrow`, mono 11px, `letter-spacing:.14em`, uppercase, `color:var(--honey)`,
|
||||
with a 20px honey rule `::before`): `Trust · the thing that makes you stay`
|
||||
- **H1** (`.head h1`, 28px / weight 650 / `letter-spacing:-0.02em`; `<em>` is **honey,
|
||||
non-italic** — `h1 em { font-style:normal; color:var(--honey) }`):
|
||||
`Memory you can ` + **`correct, age, and forget.`** ← the phrase **"correct, age, and
|
||||
forget."** gets the honey accent (everything after "Memory you can " is in `<em>`).
|
||||
- **Body** (`.head p`, 15px, `--text-muted`, `line-height:1.55`, `max-width:64ch`; `<b>` =
|
||||
`--text-2`): verbatim —
|
||||
> A memory that only grows is a liability. Waggle shows you **how sure it is**, **how
|
||||
> fresh it is**, and **where it came from** — and lets you fix or forget anything.
|
||||
> You're always in control of what the hive believes.
|
||||
|
||||
(Honey-emphasis `<b>` spans: "how sure it is", "how fresh it is", "where it came from".)
|
||||
|
||||
### View B — Why
|
||||
- **Eyebrow:** `Provenance · accountability`
|
||||
- **H1** (honey `<em>` = the quoted question):
|
||||
`Ask the agent ` + **`"why did you do that?"`**
|
||||
- **Body** (`<b>` spans bolded): verbatim —
|
||||
> Any action an agent takes can be traced back to the exact memories and sources behind
|
||||
> it — so a wrong move is **diagnosable, not mysterious**. If a bad memory caused it,
|
||||
> fix the memory right from the trace.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stat Cards — "trust summary bar" (`.tsum`, View A only)
|
||||
|
||||
4-up grid: `grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:24px`. Collapses
|
||||
to `1fr 1fr` under `max-width:820px`. Each card `.ts`: `padding:16px`, `border-radius:
|
||||
var(--r-lg)` (18px), `border:1px solid var(--line-soft)`, `background:var(--surface)`.
|
||||
Value `.ts .v` = 24px / weight 750 / `letter-spacing:-0.02em`; label `.ts .l` = 11.5px,
|
||||
`--text-muted`, `margin-top:5px`. **Warn variant** `.ts.warn`: `border-color:
|
||||
color-mix(in srgb,var(--attention) 35%,transparent)` + value tinted `--attention`.
|
||||
|
||||
| # | Value (verbatim) | Label (verbatim) | Color of value | Card variant |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `142` | `Memories in this hive` | default `--text` (white/ink) | `.ts` |
|
||||
| 2 | `128` | `High confidence & fresh` | **green** — `style="color:var(--healthy)"` (`#6cb78c`) | `.ts` |
|
||||
| 3 | `9` | `Stale · worth a review` | **honey** — `--attention` (`#e9a52c`) | `.ts.warn` |
|
||||
| 4 | `3` | `Awaiting your confirm` | **honey** — `--attention` (`#e9a52c`) | `.ts.warn` |
|
||||
|
||||
Color order = **white / green / honey / honey** (matches the brief). Cards 3 & 4 also get
|
||||
the honey-tinted warn border.
|
||||
|
||||
---
|
||||
|
||||
## 4. Search + Filters (`.toolbar`, View A only)
|
||||
|
||||
`.toolbar { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap }`.
|
||||
|
||||
**Search bar** (`.search` — flex:1, min-width:200px, `padding:9px 14px`, `border-radius:
|
||||
11px`, `border:1px solid var(--line)`, `background:var(--surface)`):
|
||||
- Leading magnifier `<svg>` (circle + handle), 16px, `stroke:var(--text-dim)`,
|
||||
`stroke-width:1.9`, no fill.
|
||||
- Input placeholder (verbatim): `Search what Waggle knows… or ask it to forget something`
|
||||
— 14px, transparent bg, `color:var(--text)`.
|
||||
|
||||
**Filter chips** (`.filt`, 12.5px / weight 600, `padding:8px 13px`, `border-radius:9px`,
|
||||
`border:1px solid var(--line-soft)`, `background:var(--surface)`):
|
||||
|
||||
| Chip (verbatim) | State |
|
||||
|---|---|
|
||||
| `All` | **active** — `.filt.on` |
|
||||
| `Stale` | default |
|
||||
| `Needs confirm` | default |
|
||||
| `Forgotten` | default |
|
||||
|
||||
**Active chip** `.filt.on`: `color:var(--text); border-color:var(--honey-line);
|
||||
background:var(--honey-wash)` (honey-wash fill + honey-line border). Hover `.filt:hover`
|
||||
just lifts `color` to `--text`. (Note: the warm primitive set already has chip-like
|
||||
patterns; the active = honey-wash + honey-line treatment must be reused.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Memory ROW anatomy (`.mem`, the core component — repeated in `.mems` grid)
|
||||
|
||||
`.mems { display:grid; gap:10px }`. Each row `.mem`: `border:1px solid var(--line-soft)`,
|
||||
`background:var(--surface)`, `border-radius:var(--r-lg)` (18px), `padding:16px 18px`,
|
||||
`transition:.15s`. **State border variants:**
|
||||
- `.mem.stale` → `border-color:color-mix(in srgb,var(--attention) 30%,var(--line-soft))`
|
||||
- `.mem.disputed` → `border-color:color-mix(in srgb,var(--risk) 30%,var(--line-soft));
|
||||
opacity:.92`
|
||||
|
||||
Layout `.mem .top { display:flex; align-items:flex-start; gap:13px }` — three columns:
|
||||
**[confidence ring] [body] [actions]**.
|
||||
|
||||
### 5a. Confidence ring (`.conf`, width 42px, flex:none, centered)
|
||||
- Ring `.ring`: `width/height 38px`, `border-radius:999px`, `display:grid; place-items:
|
||||
center`, mono 11px / weight 600. **Both the number color AND the 2px ring border come
|
||||
from `confColor(c)`** (inline `style="color:${confColor(c)};border:2px solid
|
||||
${confColor(c)}"`).
|
||||
- **Ring color logic** (`confColor(c)` JS, verbatim):
|
||||
- `c >= 85` → `var(--healthy)` (sage green)
|
||||
- `c >= 60` → `var(--attention)` (honey)
|
||||
- `else` (`< 60`) → `var(--risk)` (terracotta)
|
||||
- Caption `.cl` below ring: literal text `conf` (rendered uppercase via
|
||||
`text-transform:uppercase`), 9px mono, `--text-dim`, `margin-top:4px`,
|
||||
`letter-spacing:.06em`. → reads **`CONF`**.
|
||||
|
||||
### 5b. Body (`.mbody`, flex:1)
|
||||
- **Fact text** `.mtext`: 14.5px, `line-height:1.5`, `color:var(--text)`. **`<b>` inside
|
||||
gets weight 650** — this is the honey-free *bold* emphasis on the key phrase (NOT
|
||||
honey-colored; it's bold weight only). Disputed rows strike through: `.mem.disputed
|
||||
.mtext { text-decoration:line-through; text-decoration-color:color-mix(in srgb,
|
||||
var(--risk) 60%,transparent); color:var(--text-muted) }`.
|
||||
- **Provenance line** `.prov` (`margin-top:8px`, flex-wrap, `gap:7px 14px`, mono 10.5px,
|
||||
`color:var(--text-dim)`). Format = three segments:
|
||||
`⬡ <id>` · `source: <src>` · `● <freshness>`
|
||||
- ID segment: `<span class="src">⬡ ${m.id}</span>` — `.prov .src { color:var(--intel) }`
|
||||
(muted violet) — e.g. `⬡ M-204`.
|
||||
- Source segment: plain `source: ${m.src}` in `--text-dim` — e.g. `source: chat · Tue`.
|
||||
- Freshness segment: `● ` + label, colored by `.fresh.ok`→`var(--healthy)` (green) or
|
||||
`.fresh.old`→`var(--attention)` (honey). Fresh label = `fresh`; old label =
|
||||
`aging — last seen 6w ago`.
|
||||
|
||||
### 5c. Action buttons (`.acts`, flex:none, gap:6px)
|
||||
Two square icon buttons `.mact` (30×30px, `border-radius:8px`, `border:1px solid
|
||||
var(--line-soft)`, `background:var(--surface-2)`, `color:var(--text-muted)`; icon svg 15px
|
||||
`stroke:currentColor` `stroke-width:1.8`):
|
||||
1. **Edit / correct** — `title="Edit / correct"`, `data-act="edit"`; pencil icon. Hover
|
||||
`.mact:hover { border-color:var(--honey-line); color:var(--honey) }`. Click → toast
|
||||
`Correcting <id> — opens an inline editor`.
|
||||
2. **Forget / delete** — `.mact.danger`, `title="Forget this"`, `data-act="forget"`; trash
|
||||
icon. Danger hover `.mact.danger:hover { border-color:color-mix(in srgb,var(--risk)
|
||||
45%,transparent); color:var(--risk) }`. Click → row animates out (`opacity:0;
|
||||
translateX(-12px)`, 300ms, then removed) + toast `Forgotten <id> — removed from recall`.
|
||||
|
||||
### 5d. Inline sub-banners (conditional, render inside `.mbody`)
|
||||
- **Corrected banner** (`.corrected`, disputed rows that carry `m.corrected`): healthy-wash
|
||||
pill — `background:var(--healthy-wash); border:1px solid color-mix(in srgb,var(--healthy)
|
||||
30%,transparent)`, 12.5px `--text-2`, green check svg (`stroke:var(--healthy)`). Renders
|
||||
`<b>{before "→"}</b> → {after "→"}`.
|
||||
- **Stale review banner** (`.stalebanner`, on `.mem.stale`): honey-wash pill —
|
||||
`background:var(--honey-wash); border:1px solid var(--honey-line)`, clock svg
|
||||
(`stroke:var(--honey)`). Text (verbatim): `This is 6 weeks old — still true?` Right-aligned
|
||||
button pair `.sp`:
|
||||
- `.sbtn.go` (`background:var(--honey); color:#1a1407`) — verbatim `Still true`
|
||||
(`data-act="confirm"`). Click → banner morphs into a `.corrected` "Confirmed still true ·
|
||||
freshness reset" pill + toast `Confirmed <id> — freshness reset`.
|
||||
- `.sbtn.ghost` (`background:var(--surface); color:var(--text-2); border:var(--line-strong)`)
|
||||
— verbatim `Forget` (`data-act="forget"`).
|
||||
|
||||
### 5e. Seed dataset (the 5 demo rows — verbatim `mems[]`)
|
||||
| id | fact text (`<b>` = bold phrase) | conf | ring color | src | fresh | state |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `M-204` | Mara wants market work to **lead with the regulated-industries angle**. | 94 | green | `chat · Tue` | fresh | ok |
|
||||
| `M-198` | **Mem0** is cloud-only and raised prices ~15% in March. | 88 | green | `web · mem0.ai` | fresh | ok |
|
||||
| `M-141` | The Q3 launch date is **September 12**. | 61 | honey | `chat · 6 weeks ago` | old | **stale** (shows stale banner) |
|
||||
| `M-088` | Mara prefers **Slack over email** for updates. | 47 | terracotta | `inferred · once` | old | **disputed** (strikethrough + corrected banner: `Corrected by you → prefers a daily digest, not Slack pings.`) |
|
||||
| `M-052` | Primary competitor is **Letta**. | 90 | green | `teardown.md` | fresh | ok |
|
||||
|
||||
---
|
||||
|
||||
## 6. "Why did you do that?" Trace view (View B, `.trace`)
|
||||
|
||||
A single card: `border:1px solid var(--line)`, `border-radius:var(--r-xl)` (26px),
|
||||
`background:var(--bg-2)`, `overflow:hidden`. Three regions: **header → vertical chain →
|
||||
action footer**, then the trust principle footnote below.
|
||||
|
||||
### 6a. Trace header (`.th`, `padding:18px 22px`, bottom border)
|
||||
- **Hex avatar** `.ti.hex` (34×38px, honey gradient `linear-gradient(150deg,
|
||||
var(--honey-bright),var(--honey-deep))`, dark-ink `paper-plane`/send svg). → reuse warm
|
||||
`HexAvatar` primitive.
|
||||
- Title `<b>` 15.5px/650 (verbatim): `Drafted the board brief around "regulated industries"`
|
||||
- Subtitle `.sub` 12px `--text-muted` (verbatim): `Deck-builder · 18m ago · Q2 Board Deck`
|
||||
- `.when` (mono 11px `--text-dim`, margin-left:auto): `trace #a1f9`
|
||||
|
||||
### 6b. Trace chain (`.tchain`, `padding:8px 22px 18px`) — `.tnode` steps
|
||||
Each node is a 2-col grid `24px 1fr` with a **vertical connector line** drawn via
|
||||
`.tnode::before { position:absolute; left:11px; top:34px; bottom:-14px; width:1.5px;
|
||||
background:var(--line) }` (suppressed on `:last-child`). Each `.tdot` (24px circle,
|
||||
colored per step, dark-ink svg `stroke:#1a1407 stroke-width:2.4`):
|
||||
|
||||
| # | dot color | title `<b>` (13.5px/600) | body `.tc p` (13px `--text-muted`) — verbatim |
|
||||
|---|---|---|---|
|
||||
| 1 | `var(--intel)` (violet, arrow icon) | `Goal received` | You asked: `"tighten the board narrative."` (the quote in `.mono`) |
|
||||
| 2 | `var(--honey)` (check icon) | `Recalled 3 memories` | Strongest was: `⬡ mem #M-204` (`.src` violet) — "Mara wants market work to lead with the regulated-industries angle." + **evidence chip** `.ev`: `confidence 94% · source: chat · Tue · still fresh` |
|
||||
| 3 | `var(--honey)` (check icon) | `Cross-checked the teardown` | Confirmed 2 of 3 proof points cite customer quotes `⬡ teardown.md` (`.src`) |
|
||||
| 4 | `var(--healthy)` (green, arrow icon) | `Acted` | Wrote slide 6 around the regulated angle and flagged it for your review. |
|
||||
|
||||
`.ev` evidence chip: `font-size:12px; color:var(--text-2); padding:8px 12px; border-radius:
|
||||
8px; background:var(--surface); border:1px solid var(--line-soft)`. `.src` inline refs =
|
||||
mono 11px `--intel`. Inline `.mono` quote = mono 11.5px `--text-dim`.
|
||||
|
||||
### 6c. Trace action footer (`.traceact`, top border, `background:var(--surface)`,
|
||||
`padding:16px 22px`, gap:9px) — three buttons `.tbtn2` (12.5px/650, `padding:9px 15px`,
|
||||
`border-radius:9px`):
|
||||
1. `.tbtn2.go` (honey fill, `#1a1407` ink) — verbatim `Looks right`
|
||||
2. `.tbtn2.ghost` (`--surface-2`, `--line-strong` border) — verbatim
|
||||
`That memory is wrong → correct it`
|
||||
3. `.tbtn2.danger` (transparent, risk text + risk-tint border `color-mix(in srgb,
|
||||
var(--risk) 35%,transparent)`) — verbatim `Forget #M-204 & redo`
|
||||
|
||||
---
|
||||
|
||||
## 7. Trust principle footnote (`.principle`, both views)
|
||||
|
||||
Shared component below the content: `display:flex; gap:13px; align-items:flex-start;
|
||||
padding:18px 20px; border-radius:var(--r-lg); background:var(--bg-2); border:1px solid
|
||||
var(--line-soft)`. Leading 20px svg `stroke:var(--healthy)` (shield/check on View A;
|
||||
info-circle on View B). Text 13.5px `--text-muted` `line-height:1.6`, `<b>`=`--text`.
|
||||
|
||||
- **View A (verbatim):** **Nothing is remembered behind your back.** Every memory is
|
||||
inspectable, editable, and forgettable — and forgetting is real: it's removed from recall
|
||||
and from anything Waggle says next. Confidence and freshness are shown so the agent (and
|
||||
you) can discount what's old or shaky instead of acting on it blindly.
|
||||
- **View B (verbatim):** **Every agent action keeps its trace.** The chain from goal →
|
||||
recalled memories → checks → action is stored with the result, so "why did you do that?"
|
||||
always has an answer — and the fix (correct or forget the offending memory) is one click
|
||||
from the explanation.
|
||||
|
||||
**Toast** (shared, `#toast`): `.toast` bottom-center pill, `background:var(--surface);
|
||||
border:1px solid var(--healthy); box-shadow:var(--shadow-lg)`, 8px green dot `.td2`
|
||||
(`background:var(--healthy)`), slides up on `.show` (2400ms auto-dismiss).
|
||||
|
||||
---
|
||||
|
||||
## 8. Tokens — mock → PR3 warm tokens (1:1, already aligned)
|
||||
|
||||
The mock's `waggle.css` tokens are **identical hex** to PR3's warm tokens in
|
||||
`apps/web/src/index.css` (same names, same dark + light values). **No remapping needed** —
|
||||
build directly against the CSS vars below.
|
||||
|
||||
| Mock var | Hex (dark / light) | PR3 warm token | Used in screen 19 for |
|
||||
|---|---|---|---|
|
||||
| `--honey` | `#e9a52c` / `#b57d12` | `--honey` ✓ | segment-on, accent `<em>`, eyebrow, conf 60–84, active filter, go-buttons |
|
||||
| `--honey-bright` | `#f6c45a` / `#cf932a` | `--honey-bright` ✓ | hex-avatar gradient top |
|
||||
| `--honey-deep` | `#c07e16` / `#92620a` | `--honey-deep` ✓ | hex-avatar gradient bottom |
|
||||
| `--honey-wash` | `rgba(233,165,44,.10)` | `--honey-wash` ✓ | active filter bg, stale banner bg |
|
||||
| `--honey-line` | `rgba(233,165,44,.28)` | `--honey-line` ✓ | active filter border, stale border, hover states |
|
||||
| `--healthy` | `#6cb78c` / `#3c8a5f` | `--healthy` ✓ | stat #2 green, conf ≥85 ring, fresh●, "Acted" dot, principle icon, toast |
|
||||
| `--healthy-wash` | `rgba(108,183,140,.12)` | `--healthy-wash` ✓ | corrected banner bg |
|
||||
| `--attention` | `#e9a52c` / `#b57d12` | `--attention` ✓ | stat #3/#4 honey, conf 60–84 ring, stale border, aging● |
|
||||
| `--risk` | `#db8068` / `#c0573c` | `--risk` ✓ | conf <60 ring, disputed border/strikethrough, danger buttons |
|
||||
| `--intel` | `#b196dd` / `#7d57b8` | `--intel` ✓ | `⬡` provenance id, trace `.src` refs, "Goal received" dot |
|
||||
| Dark-ink on honey | `#1a1407` (mock uses `#1a1407`) | warm light `--primary-foreground: #1a1407` ✓ | text on every honey fill |
|
||||
| `--surface` / `--surface-2` / `--bg-2` | warm graphite / paper | `--surface` / `--surface-2` / `--bg-2` ✓ | cards, rows, action btns, trace bg |
|
||||
| `--line` / `--line-soft` / `--line-strong` | warm graphite lines | same ✓ | borders |
|
||||
| `--text` / `--text-2` / `--text-muted` / `--text-dim` | warm text scale | same ✓ | type hierarchy |
|
||||
| `--r-lg` 18px / `--r-xl` 26px | radii | same ✓ | cards/rows 18px, trace 26px |
|
||||
| `--mono` JetBrains Mono / `--sans` Hanken Grotesk | fonts | same ✓ | provenance/conf mono; body sans |
|
||||
|
||||
**Reuse existing warm primitives** (`apps/web/src/components/os/warm/`):
|
||||
- `ProvenanceLine.tsx` — already renders `⬡ source · when` in mono `--intel` (the §5b
|
||||
provenance pattern). Extend with a freshness `●` segment + `source:` prefix to match.
|
||||
- `HexAvatar.tsx` — the §6a honey-gradient hex with dark-ink icon.
|
||||
- `SectionLabel.tsx` — eyebrow/mono-uppercase labels.
|
||||
- `DotLive.tsx` — the freshness `●` dot.
|
||||
- `ModelPill` / `RunChip` / chip patterns — the filter-chip + go-button shapes.
|
||||
|
||||
---
|
||||
|
||||
## Component inventory (6-line summary)
|
||||
|
||||
1. **Header/segmented control** — `Memory Trust · view` eyebrow + 2-tab switch (`Manage memory` default-on honey-fill | `Why did you do that?`) + swapping `.vlabel` + theme toggle, toggling between two full views.
|
||||
2. **Editorial hero (per view)** — honey eyebrow, 28px H1 with honey `<em>` accent ("correct, age, and forget." / "why did you do that?"), 64ch body with bold spans.
|
||||
3. **Stat bar (View A)** — 4 cards: 142 white / 128 green / 9 honey-warn / 3 honey-warn, repeat(4,1fr)→2-col @820px.
|
||||
4. **Toolbar (View A)** — search input ("Search what Waggle knows… or ask it to forget something") + 4 filter chips (All on / Stale / Needs confirm / Forgotten).
|
||||
5. **Memory row** — 3-col [conf ring (color by ≥85 green / ≥60 honey / <60 terracotta) + CONF] · [bold fact + `⬡ id · source · ● freshness` provenance + optional corrected/stale banner] · [edit + danger-forget icon buttons]; 5 seed rows incl. 1 stale + 1 disputed/strikethrough.
|
||||
6. **Why-trace (View B)** — hex-avatar header (`trace #a1f9`) + 4-node connector chain (Goal→Recalled→Cross-checked→Acted, dots intel/honey/honey/healthy, evidence chip) + 3-button footer (Looks right / correct it / Forget #M-204 & redo); shared trust-principle footnote + green toast on both views.
|
||||
249
docs/redesign-warm-hive/pr35-recon/05-memory-store-api.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# PR3.5 Memory-Trust — Real Memory-Store API Surface
|
||||
|
||||
> Recon for the warm-Hive PR3.5 Memory-Trust arc. Question: what can the
|
||||
> Memory-Trust UI wire to **as it exists today**, vs what must be added —
|
||||
> and what it must **never fabricate**.
|
||||
>
|
||||
> Repo: `D:/Projects/waggle-os`. Substrate: `packages/hive-mind-core/src/mind/`.
|
||||
> Server surface: `packages/server/src/local/routes/`.
|
||||
> All claims below are quoted file:line. **Brutally honest split: PR3.5 must not invent data the store doesn't hold.**
|
||||
|
||||
---
|
||||
|
||||
## TL;DR Capability Matrix
|
||||
|
||||
| Feature | Verdict | Where the truth lives |
|
||||
|---|---|---|
|
||||
| **source** (provenance class) | ✅ **REAL** (stored column) | `memory_frames.source` |
|
||||
| **forget** (delete) | ✅ **REAL** (store + 2 routes) | `FrameStore.delete()` + `DELETE /api/memory/:id` |
|
||||
| **correct** (edit content) | ✅ **REAL** (store + 2 routes) | `FrameStore.update()` + `PATCH /api/memory/:id` |
|
||||
| **freshness / staleness** | 🟡 **DERIVABLE** (compute fn exists; not surfaced) | `computeTemporalScore()` over `created_at` / `last_accessed` |
|
||||
| **confidence** | 🟡 **DERIVABLE / PARTIAL** (metadata blob; only set at harvest) | `memory_frames.metadata.confidence` (B2 heuristic) |
|
||||
| **trace** ("why did you do that?") | 🟡 **DERIVABLE** (rich store + 1 route; not per-frame) | `execution_traces` + `GET /api/agents/:id/traces` |
|
||||
| **confirm / verified status** | 🔴 **MUST-BUILD** (no per-frame confirm concept) | absent — see §4 |
|
||||
|
||||
**One-line split:** `source`, `forget`, `correct` are **REAL and route-exposed today**. `freshness`, `confidence`, and `trace` are **DERIVABLE** (the signals exist in the substrate but are not projected onto the Chat/Workspace surfaces, and confidence is only populated on the harvest path). A per-memory **"confirm / needs-confirm / verified"** status is **MUST-BUILD** — it does not exist.
|
||||
|
||||
---
|
||||
|
||||
## 1. The FRAME shape
|
||||
|
||||
Canonical row type — `packages/hive-mind-core/src/mind/frames.ts:27-48`:
|
||||
|
||||
```ts
|
||||
export interface MemoryFrame {
|
||||
id: number;
|
||||
frame_type: FrameType; // 'I' | 'P' | 'B'
|
||||
gop_id: string;
|
||||
t: number;
|
||||
base_frame_id: number | null;
|
||||
content: string;
|
||||
importance: Importance; // critical|important|normal|temporary|deprecated
|
||||
source: FrameSource; // see below
|
||||
access_count: number;
|
||||
created_at: string;
|
||||
last_accessed: string;
|
||||
content_hash?: string | null; // dedup
|
||||
metadata?: string; // JSON blob (Phase 2B) — provenance/classification
|
||||
}
|
||||
```
|
||||
|
||||
DDL backing it — `schema.ts:47-73` (`memory_frames`). Every field above is a real
|
||||
stored column; `metadata` is `TEXT NOT NULL DEFAULT '{}'` (`schema.ts:65`).
|
||||
|
||||
Per-field verdict against the requested set:
|
||||
|
||||
- **id** — REAL (`frames.ts:28`, PK `schema.ts:48`).
|
||||
- **content / text** — REAL (`frames.ts:33`, `schema.ts:53`). The field is `content`, NOT `text`.
|
||||
- **source** — ✅ **REAL stored column.** `frames.ts:25` defines `FrameSource = 'user_stated' | 'tool_verified' | 'agent_inferred' | 'import' | 'system' | 'personal' | 'workspace' | 'team_sync'`. The DB CHECK is narrower — `schema.ts:56-57` only allows `('user_stated','tool_verified','agent_inferred','import','system')` (the `personal/workspace/team_sync` members are search-time mind labels, not persisted provenance — see `memory.ts:41-43`).
|
||||
- **confidence / score** — 🟡 **NOT a frame column.** No `confidence` or `score` column on `memory_frames`. Two distinct things wear the name:
|
||||
- `score` is a **search-time, computed** ranking field added by HybridSearch (returned in `normalizeFrame` at `memory.ts:58`), never stored.
|
||||
- `confidence` (0-100) is a **derived metadata field** that rides `memory_frames.metadata` JSON, projected by `normalizeToMemory` at `memory-center.ts:100` (`typeof meta.confidence === 'number' ? meta.confidence : undefined`). It is only **populated on the harvest path** (`harvestConfidence`, see §"confidence" below). Curated/agent/quick-capture writes leave it `undefined`.
|
||||
- (`confidence REAL` does exist — but on `knowledge_relations`, `schema.ts:109`, not on frames.)
|
||||
- **created_at** — REAL (`frames.ts:37`, `schema.ts:59`, default `datetime('now')`).
|
||||
- **updated_at** — 🔴 **NOT on frames.** The frame table has no `updated_at` column. `identity`/`procedures` tables have one (`schema.ts:20,166`); frames do not. An `updatedAt` is faked into the `metadata` blob on PATCH (`memory-center.ts:336`). The shared `Memory.updatedAt` reads it from metadata (`memory-center.ts:112`).
|
||||
- **accessed_at / last_used** — REAL: `last_accessed` (`frames.ts:38`, `schema.ts:60`), bumped by `touch()` (`frames.ts:184-191`). `access_count` (`frames.ts:36`) is the use counter.
|
||||
- **decay / staleness** — 🟡 **NOT stored; DERIVABLE.** No decay column. Decay is a pure compute over timestamps in `scoring.ts` (see §5).
|
||||
- **type / kind** — split brain:
|
||||
- `frame_type` (`'I' | 'P' | 'B'`) is the substrate-internal kind (incremental/patch/branch), REAL (`frames.ts:29`).
|
||||
- `importance` (`critical|important|normal|temporary|deprecated`) is the closest stored "salience" axis, REAL (`frames.ts:35`).
|
||||
- The product-facing `MemoryKind` (`fact|decision|task|preference|strategy|learning|goal|entity`, `shared/types.ts:354-356`) is **NOT a column** — it rides `metadata.kind`, projected at `memory-center.ts:91`, defaulting to `'fact'` when absent.
|
||||
|
||||
**Verdict — {confidence, freshness/recency, source}:**
|
||||
- **source** → REAL stored field.
|
||||
- **freshness/recency** → DERIVED (no column; computed from `created_at`/`last_accessed`).
|
||||
- **confidence** → metadata-blob field, REAL-but-sparse (only harvest sets it); treat as DERIVABLE/PARTIAL for any non-harvested frame.
|
||||
|
||||
---
|
||||
|
||||
## 2. FORGET (delete) — ✅ REAL
|
||||
|
||||
Store: `FrameStore.delete(id): boolean` — `frames.ts:321-334`. Hard delete; cleans up
|
||||
FTS, vec, KG entity links, and nullifies self-referential FKs. There is **no
|
||||
tombstone** — it's a real row removal. Also `deleteByContentPrefix` (`frames.ts:343-354`).
|
||||
|
||||
Routes that expose it:
|
||||
- `DELETE /api/memory/frames/:id` — `memory.ts:571-607` (legacy frame-id contract; workspace-first then personal fallback).
|
||||
- `DELETE /api/memory/:id` — `memory-center.ts:390-411` (bare-id contract, hard delete, `mind`-strict).
|
||||
|
||||
Both emit an audit event (`eventType: 'memory_delete'`, `memory.ts:601`, `memory-center.ts:402`).
|
||||
**Suggested approach:** wire the Memory-Trust "Forget" action straight to `DELETE /api/memory/:id?mind=…`. No new backend.
|
||||
|
||||
---
|
||||
|
||||
## 3. CORRECT (edit content) — ✅ REAL
|
||||
|
||||
Store: `FrameStore.update(id, content, importance?)` — `frames.ts:281-301`. Updates the
|
||||
row, FTS index, vec index, and maintains `content_hash`. Returns the updated frame.
|
||||
|
||||
Routes:
|
||||
- `PUT /api/memory/frames/:id` — `memory.ts:466-533` (content + importance; XSS-sanitized `memory.ts:483`).
|
||||
- `PATCH /api/memory/:id` — `memory-center.ts:296-352` (content/importance **and** metadata classification: kind/scope/tags/status/title/evidence; stamps `metadata.updatedAt`).
|
||||
|
||||
**Suggested approach:** "Correct this memory" → `PATCH /api/memory/:id`. It already supports
|
||||
editing content and reclassifying. No new backend.
|
||||
|
||||
---
|
||||
|
||||
## 4. CONFIRM / "needs confirm" / verified status — 🔴 MUST-BUILD (mostly)
|
||||
|
||||
**There is NO per-frame "confirmed / needs-confirmation / verified" concept.** Honest accounting of the near-misses:
|
||||
|
||||
- `FrameSource` has a `tool_verified` member (`frames.ts:25`, `schema.ts:57`) — but that's a **provenance class set at write time** (this fact came from a verified tool call), not a user-confirmation lifecycle. It's never toggled after creation.
|
||||
- `execution_traces.outcome` has a `'verified'` value (`schema.ts:223`, `execution-traces.ts:20`) — but that's about an **agent run** passing a verifier gate, not a memory being confirmed.
|
||||
- The shared `MemoryStatus` union (`shared/types.ts:514-515`) has `'unreviewed' | 'low_confidence' | 'conflict'` — the **vocabulary for a review lifecycle exists** and is already projected (`memory-center.ts:84-85`) and filterable (`memory-center.ts:198`). But: nothing **writes** `unreviewed` today (the create path stamps `'active'`, `memory-center.ts:273`; harvest commit is the only intended `unreviewed` producer per `types.ts:506-507` but that write was not confirmed in this recon), and `low_confidence`/`conflict` are explicitly noted as *"may be derived at recall-time rather than persisted"* (`types.ts:510-511`) — i.e. not implemented.
|
||||
|
||||
**Verdict:** A "Confirm" / "Needs your confirmation" affordance is **MUST-BUILD**, but cheaply:
|
||||
the `metadata.status` field + the `MemoryStatus` union are the rails. Add a
|
||||
`POST /api/memory/:id/confirm` that sets `metadata.status='active'` (clearing `unreviewed`),
|
||||
mirroring the existing `/archive` route (`memory-center.ts:354-386`). The "needs confirm"
|
||||
queue = `GET /api/memory?status=unreviewed` (already works — `memory-center.ts:198`).
|
||||
**Do NOT show a "verified ✓" badge unless the frame is genuinely `source==='tool_verified'` or `status` was explicitly set** — anything else is fabrication.
|
||||
|
||||
---
|
||||
|
||||
## 5. STALE / freshness / decay — 🟡 DERIVABLE (not surfaced)
|
||||
|
||||
Real signal exists as **pure compute**, not a stored flag — `scoring.ts`:
|
||||
|
||||
- `computeTemporalScore(iso)` — `scoring.ts:52-63`. Returns `1.0` if within **7 days**
|
||||
(`RECENCY_BOOST_DAYS`, `scoring.ts:38`), else exponential decay with a **30-day half-life**
|
||||
(`HALF_LIFE_DAYS`, `scoring.ts:37`): `Math.pow(0.5, daysSince / 30)`.
|
||||
- Decay anchors on **`created_at`** (write time), NOT `last_accessed` — deliberate
|
||||
(`scoring.ts:91-97`): `last_accessed` is bumped by `touch()` on every read, so decaying on
|
||||
it made the dimension constant noise. Use `created_at` for "age".
|
||||
- The substrate also auto-prunes by age in `FrameStore.compact()` — temporary frames > 30d,
|
||||
deprecated > 90d (`frames.ts:366-389`) — a real staleness policy, but a background sweep, not a per-frame badge.
|
||||
|
||||
**Verdict:** "Stale · worth a review" is **DERIVABLE today** with zero new storage:
|
||||
compute `daysSince(created_at)` (or call `computeTemporalScore`) FE-side or in a thin route.
|
||||
A reasonable "stale" threshold: temporal score below ~0.5 ≈ older than one half-life (~30d),
|
||||
or simply `created_at` older than N days for `importance ∈ {normal, temporary}`.
|
||||
**Constraint:** `created_at` IS projected on the legacy `/api/memory/frames` and `/search`
|
||||
responses (`memory.ts:56`), so the FE already has the input. **Do NOT invent a "freshness %"
|
||||
that implies stored decay** — present it as "last touched / age", computed honestly.
|
||||
|
||||
---
|
||||
|
||||
## 6. "WHY DID YOU DO THAT?" trace — 🟡 DERIVABLE (rich; not per-frame)
|
||||
|
||||
A real, rich execution-trace store exists — `execution-traces.ts` + `execution_traces`
|
||||
DDL (`schema.ts:215-233`). Per "unit of agent work" it records:
|
||||
|
||||
- `outcome` (`success|corrected|abandoned|verified|pending`, `execution-traces.ts:20`),
|
||||
`cost_usd`, `duration_ms`, `created_at`/`finalized_at`, `session_id`/`persona_id`/
|
||||
`workspace_id`/`model` (`execution-traces.ts:75-88`).
|
||||
- A structured `trace_json` payload (`TracePayload`, `execution-traces.ts:48-72`):
|
||||
**`input`, `output`, `reasoning[]` (step text + ts), `toolCalls[]` (tool, args, result, ok,
|
||||
durationMs, ts — `execution-traces.ts:22-36`), `artifacts[]`, `tokens`, optional `harness`
|
||||
gate results, and `correctionFeedback`.** This is exactly the data a "why did you do that?"
|
||||
panel needs.
|
||||
|
||||
Production wiring (it IS populated for chat):
|
||||
- The chat loop creates a `TraceRecorder` and `start()`s a trace per turn
|
||||
(`chat.ts:1277-1286`), records reasoning + tool calls through it, and `finalize()`s with
|
||||
outcome+output+tokens (`chat.ts:1413-1424`); aborted/errored turns finalize as
|
||||
`'abandoned'` (`chat.ts:1648-1649`, `1713`).
|
||||
- Corrections downgrade a prior trace via `markCorrected()` (`execution-traces.ts:297-310`,
|
||||
feedback noted at `chat.ts:1651`).
|
||||
|
||||
Route exposure:
|
||||
- `GET /api/agents/:id/traces` — `agents.ts:469-505`. Returns id/ts/session/workspace/model/
|
||||
**outcome/cost/durationMs/tools[]** per trace. **It filters by the `agent:{id}` tag**
|
||||
(`agents.ts:486-487`).
|
||||
|
||||
**Two honest gaps for PR3.5:**
|
||||
1. **The chat trace `start()` does NOT pass a `tags:['agent:…']`** (`chat.ts:1279-1285`),
|
||||
so the agent-traces route's tag filter will **not** surface conversational traces. The
|
||||
richest traces (chat reasoning + tool calls) are written but not addressable by that route.
|
||||
2. **No frame↔trace backlink.** Nothing links a saved `memory_frame` to the
|
||||
`execution_trace` that produced it (no `trace_id` column, no metadata field; grep of
|
||||
chat/home/workspace routes for any `traceId`/`trace_id` link returned nothing). So
|
||||
"why is THIS specific memory here?" can't be answered from the trace store today —
|
||||
you can only show "what the agent did in this turn/session", not "the decision that wrote this frame".
|
||||
|
||||
**Verdict:** Trace data is **DERIVABLE and rich** for the session/turn granularity, but a
|
||||
**per-frame "why" requires MUST-BUILD plumbing** (a `trace_id` on the frame metadata at
|
||||
write time, plus a `GET /api/memory/:id/trace` resolver). For PR3.5, the **cheap honest win**
|
||||
is a session/turn-scoped trace view (reasoning steps + tool calls) — and either (a) add the
|
||||
`agent:` tag to chat `start()`, or (b) add a thin `GET /api/sessions/:id/traces` reading
|
||||
`traceStore.queryParsed({ sessionId })` (the store already supports it, `execution-traces.ts:328`).
|
||||
**Do NOT synthesize a "reason" string for a frame that has no linked trace.**
|
||||
|
||||
---
|
||||
|
||||
## 7. SERVER ROUTES exposing memory to apps/web
|
||||
|
||||
Two plugins. **Legacy frame ops** (`memory.ts`) + **shared-`Memory`-entity contract**
|
||||
(`memory-center.ts`). Quoted with method + path + file:line:
|
||||
|
||||
### `packages/server/src/local/routes/memory.ts`
|
||||
| Method | Path | Line | Notes |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/memory/search?q&scope&limit&workspace&since&until` | `memory.ts:126` | HybridSearch; returns normalized frames incl. computed `score`, `source` |
|
||||
| GET | `/api/memory/frames?workspace&limit&since&until` | `memory.ts:194` | recent frames, no query needed (Memory tab initial load) |
|
||||
| POST | `/api/memory/frames` | `memory.ts:245` | direct write; optional entity extraction; XSS-sanitized; dedup |
|
||||
| GET | `/api/memory/stats?workspace&scope` | `memory.ts:397` | counts only (mind-isolation: `?scope=all-minds` opt-in, `memory.ts:425-438`) |
|
||||
| PUT | `/api/memory/frames/:id` | `memory.ts:467` | **CORRECT** — edit content/importance |
|
||||
| PATCH | `/api/memory/frames/:id/access` | `memory.ts:539` | atomic `access_count++` (touch) |
|
||||
| DELETE | `/api/memory/frames/:id` | `memory.ts:571` | **FORGET** — hard delete |
|
||||
| POST | `/api/quick-capture` | `memory.ts:614` | Home quick-capture → frame (+ awareness task row) |
|
||||
|
||||
### `packages/server/src/local/routes/memory-center.ts` (shared `Memory` shape)
|
||||
| Method | Path | Line | Notes |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/memory?mind&kind&status&scope&q&minConfidence&limit` | `memory-center.ts:167` | list as `Memory`; **status/confidence filters live here** |
|
||||
| GET | `/api/memory/:id` | `memory-center.ts:215` | one memory, normalized |
|
||||
| POST | `/api/memory` | `memory-center.ts:235` | curated create (sets `metadata.kind/scope/status='active'/confidence`) |
|
||||
| PATCH | `/api/memory/:id` | `memory-center.ts:296` | **CORRECT** — content + reclassify; stamps `updatedAt` |
|
||||
| POST | `/api/memory/:id/archive` | `memory-center.ts:355` | reversible Archive (`status='archived'`) — template for a `/confirm` route |
|
||||
| DELETE | `/api/memory/:id` | `memory-center.ts:390` | **FORGET** — hard delete, mind-strict |
|
||||
| POST | `/api/memory/merge` | `memory-center.ts:416` | merge ≥2 → concat + archive originals (C11) |
|
||||
|
||||
**Projection note (load-bearing for the warm-Hive provenance pill):** the legacy
|
||||
`memory.ts` `normalizeFrame` DOES carry `source` provenance (`memory.ts:48-49`), and
|
||||
`memory-center.ts` `normalizeToMemory` carries `source` + `sourceUrl`/`sourceId`/`confidence`
|
||||
(`memory-center.ts:97-100`). **BUT** the Chat and Workspace *context* surfaces do not read
|
||||
those routes — `workspace-context.ts:283-284` selects only `content, importance, created_at`
|
||||
from `memory_frames`, omitting `source`. **This is the "`frame.source` 1-field server
|
||||
projection" gap flagged in the S2 handoff** — adding `source` (and `created_at`, already there)
|
||||
to that projection is the MUST-BUILD that unlocks the ⬡ provenance pill on Chat + Workspace.
|
||||
|
||||
---
|
||||
|
||||
## PR3.5 build guidance (do-not-fabricate checklist)
|
||||
|
||||
| UI affordance | Wire to | New work |
|
||||
|---|---|---|
|
||||
| ⬡ provenance pill (source) | `Memory.source` from `/api/memory` ✅; for Chat/Workspace context add `source` to `workspace-context.ts:283` SELECT | 1-field projection (MUST-BUILD, tiny) |
|
||||
| Forget button | `DELETE /api/memory/:id` ✅ | none |
|
||||
| Correct / edit | `PATCH /api/memory/:id` ✅ | none |
|
||||
| "Stale · review?" | compute from `created_at` (already projected) via `computeTemporalScore` | FE compute / thin helper (DERIVABLE) |
|
||||
| Confidence chip | `Memory.confidence` (only present on harvested frames) | show **only when present**; never default a number (PARTIAL) |
|
||||
| Confirm / needs-confirm | `POST /api/memory/:id/confirm` (set `metadata.status`) + `GET /api/memory?status=unreviewed` | new route mirroring `/archive` (MUST-BUILD, cheap) |
|
||||
| "Why did you do that?" | `GET /api/agents/:id/traces` (session-level) | per-frame "why" needs a `trace_id` backlink (MUST-BUILD); session/turn view is DERIVABLE |
|
||||
|
||||
**Hard rule:** confidence, freshness, and trace-reason are the three places PR3.5 could
|
||||
silently fabricate. Confidence is sparse (harvest-only) → hide when absent. Freshness has no
|
||||
stored decay → present as honest age, not a stored %. Per-frame "why" has no backlink → only
|
||||
show a reason when a real linked trace exists.
|
||||
249
docs/redesign-warm-hive/pr4-recon/01-marketplace-fe.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# PR4 Recon · Slice 1 — Current Marketplace Front-End
|
||||
|
||||
> Read-only recon for PR4 (warm-Hive). Maps the **current** Marketplace FE against the
|
||||
> screen-09 contract (Variation A browse+ask, Variation B inline-in-chat, shared install
|
||||
> "sync" store). Cites real code; flags the delta; names exact integration points.
|
||||
> Branch: `feature/warm-hive-pr4`. No source was modified.
|
||||
|
||||
---
|
||||
|
||||
## 1. What exists today
|
||||
|
||||
The Marketplace surface today is the **UX-Refactor Phase-4B "consolidated Extend
|
||||
surface"** — a faceted *package/extension browser*, NOT the agent-searchable shelf the
|
||||
design contract describes. There are two real surfaces:
|
||||
|
||||
- **A grid browser** — `MarketplaceApp.tsx` (route `/marketplace` → `MarketplaceRoute.tsx` → `SurfaceBoundary`).
|
||||
- **An inline-in-chat install card** — `CapabilityRequestCard.tsx`, parsed out of agent
|
||||
text by `capability-request-parser.ts` (`segmentText`) inside `TextBlock`.
|
||||
|
||||
These two surfaces are **independent** — they do **not** share an install store, do not
|
||||
share micro-state vocabulary, and do not cross-reflect (installing in one does not update
|
||||
the other). That is the single biggest structural gap vs the screen-09 "sync" mandate.
|
||||
|
||||
### 1a. The grid browser — `MarketplaceApp.tsx`
|
||||
|
||||
Layout (top to bottom), all in one file, 418 LOC:
|
||||
|
||||
- **Header** (`apps/web/src/components/os/apps/MarketplaceApp.tsx:273-331`): a `Store` icon
|
||||
(honey via `var(--honey-500)`), title "Marketplace", and a right-aligned
|
||||
`"{visible.length} extensions"` count (`:277`). This count is a *render-time array
|
||||
length*, NOT a per-workspace "N in this workspace" chip bar.
|
||||
- **Two tabs** — `Browse | Audit` (`:283-297`), `role="tablist"`. Audit renders
|
||||
`InstallAuditPanel` (the C18 shared install-audit feed). Browse is the grid.
|
||||
- **B7 facet rail** (`:302-317`): pills for `['all', ...EXTENSION_TYPES]` =
|
||||
**All · Skills · Agents · Connectors · MCPs · Models · Templates** (7 facets, labels in
|
||||
`FACET_LABELS` `:37-45`). `EXTENSION_TYPES` is canonical in
|
||||
`packages/shared/src/types.ts:375-378` (`skill·agent·connector·mcp·model·template`).
|
||||
- **A keyword search input** (`:319-328`) — a plain `<Input>` with a `Search` icon,
|
||||
placeholder "Search skills, agents, connectors, MCPs, models, templates…". Debounced
|
||||
300ms (`:202-209`); marketplace facets re-query the server, others client-filter.
|
||||
- **Body** (`:334-385`): federated-provenance note (`:339-343`), loading / error+Retry /
|
||||
empty states (honest, not fake-empty), then a vertical list of `<ExtensionCard>`.
|
||||
|
||||
Per-card UI — `ExtensionCard.tsx`:
|
||||
- A generic `Package` icon (NOT a per-kind badge), name, an Installed/Available
|
||||
`StatusBadge`, optional scan-status badge.
|
||||
- A row of small text chips: **`ext.type`** (this is the only "kind badge" today — a plain
|
||||
text pill `:44`), optional category, optional trust, and a muted `ext.source` label.
|
||||
- A single right-side action that is **lifecycle/kind-aware but NOT type-aware**:
|
||||
- installable + not installed → **"Install"** (Download icon) — same label for every type.
|
||||
- installable + installed (kind `package`) → **"Remove"** (Trash icon, destructive).
|
||||
- federated (connector/agent/mcp-catalog/model/template) → **"Open in <app>"** deep-link
|
||||
(`ExternalLink`), e.g. "Connector Hub", "Agent Center", "MCP Hub" (`:72-79`).
|
||||
- There is **no install count** rendered, no progress→done micro-states beyond a single
|
||||
spinner on the Install button (`installing` → `<Loader2>` `:68`).
|
||||
|
||||
### 1b. Data flow — federate-at-read (A5)
|
||||
|
||||
The pure merge/normalize layer is `apps/web/src/lib/extension-catalog.ts` (no adapter
|
||||
import — unit-testable). `loadFacet` (`MarketplaceApp.tsx:116-188`) fans out to per-domain
|
||||
adapter calls based on the active facet, `Promise.allSettled`-merges, then
|
||||
`sortExtensions`. The mapping (from the file header `extension-catalog.ts:7-14`):
|
||||
|
||||
| facet | adapter call | normalizer | install path | installable |
|
||||
|-----------|------------------------------------------------|-----------------------|---------------------|-------------|
|
||||
| skill | `getMarketplace({type:'skill'})` + `getMarketplacePacks()` | `fromMarketplacePackage` / `fromSkillPack` | real (POST /api/marketplace/install) | pkg ✓ / pack ✗ |
|
||||
| mcp | `getMcps()` + `getMarketplace({type:'mcp'})` | `fromMcpCatalogRow` / `fromMarketplacePackage` | catalog → MCP Hub; registry-pkg → real | catalog ✗ / pkg ✓ |
|
||||
| agent | `getPersonas()` | `fromPersona` | local (Agent Center) | ✗ |
|
||||
| connector | `getConnectors()` | `fromConnector` | local (Connector Hub)| ✗ |
|
||||
| model | `getModels()` | `fromModel` | local (Settings) | ✗ |
|
||||
| template | `getWorkspaceTemplates()` | `fromTemplate` | local (Home) | ✗ |
|
||||
|
||||
Install actions (`MarketplaceApp.tsx`):
|
||||
- `handleInstall` (`:224-251`) — only `kind==='package'` installs here; goes through the
|
||||
shared `ApprovalModal` first (`buildInstallRequest` `:76-92`, scan-derived risk via
|
||||
`installRiskFor`/`classifyInstallRisk`). 403 `TIER_INSUFFICIENT` → dispatches
|
||||
`waggle:tier-insufficient` (UpgradeModal); 403 SecurityGate block → destructive toast.
|
||||
On success it **optimistically flips local state** (`:231`) — this is the *only* "sync",
|
||||
and it is purely local to this component's `extensions` array.
|
||||
- `handleUninstall` (`:253-262`) — confirmed via a second `ApprovalModal`
|
||||
(`buildRemoveRequest` `:65-74`).
|
||||
- `handleOpenIn` (`:264-266`) — dispatches `waggle:open-app` for federated deep-links.
|
||||
|
||||
Adapter methods consumed (all in `apps/web/src/lib/adapter.ts`):
|
||||
`getMarketplace` (`:2082-2092`, `/api/marketplace?type=…`), `getMarketplacePacks`
|
||||
(`:1309-1313`), `installMarketplacePackage` (`:1338-1343`, `fetchRaw` so status survives),
|
||||
`uninstallMarketplacePackage` (`:1345-1350`), `searchMarketplace` (`:1334-1336`, used by
|
||||
the inline card), `getMcps` (`:2004-2011`), `getConnectors` (`:1958-1964`),
|
||||
`connectConnector` (`:1971-1979`, `/api/connectors/:id/connect`), `installMcp`
|
||||
(`:2017-2029`, `/api/mcps/install`), `getPersonas`, `getModels`, `getWorkspaceTemplates`,
|
||||
`getExtendAudit` (`:2095-2103`, C18 audit feed).
|
||||
|
||||
### 1c. The inline-in-chat card — `CapabilityRequestCard.tsx`
|
||||
|
||||
Parsed from agent text by `capability-request-parser.ts:segmentText` — two patterns: a
|
||||
structured HTML marker `<!--waggle:capability_request {...}-->` (Pattern A) and a legacy
|
||||
`` `install_capability` with name "X" and source "Y" `` phrasing (Pattern B). Card shows
|
||||
name, a source pill, optional `reason` ("why"), and Install/Dismiss. Phases:
|
||||
`pending → installing → installed | declined | failed` (`:17`). For marketplace kind it
|
||||
**resolves name → packageId via `searchMarketplace`** then `installMarketplacePackage`;
|
||||
for skill kind it calls `adapter.installPack`. 403 → `waggle:tier-insufficient`.
|
||||
|
||||
This card is the seed of screen-09 Variation B but it is **skill/marketplace-only** (no
|
||||
connector or MCP kind), has **no vault-aware approval** ("token goes to your vault"), no
|
||||
"connected follow-up", and shares **no state** with the grid.
|
||||
|
||||
### 1d. Test coverage — `phase4b-marketplace-extend.test.tsx`
|
||||
|
||||
11 tests, all green, pinning the *current* Phase-4B contract: federate-at-read of all six
|
||||
domains, honest federated notes, browse-only packs (no pack install testid), registry-MCP
|
||||
packages install through the real route, ApprovalModal-gated install with scan-derived
|
||||
risk, 403-tier-vs-403-securitygate split, all-backends-down error+Retry, Remove confirm,
|
||||
failed-uninstall does-not-flip-installed, and the Audit tab type filter. **Any PR4 rework
|
||||
of MarketplaceApp must update / supersede these** — they assert the current testids
|
||||
(`extension-install-pkg:7`, `extension-facets`, `federated-note`) and the
|
||||
`{ type, limit:30 }` call shape.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gap vs screen-09 Variation A (the delta)
|
||||
|
||||
| screen-09 element | current state | delta |
|
||||
|---|---|---|
|
||||
| **Centered agent-search bar** ("Describe what you want to do…" → "Ask the agent") | plain keyword `<Input>` that filters/queries the registry; no agent round-trip | MISSING — no NL→agent-suggestion call; search is literal substring/registry-`?query=`. |
|
||||
| **Example chips** | none | MISSING. |
|
||||
| **Agent-suggestion box** (recommends a connector + skill + tool, each with a "why" + install button) | none in the grid (only the unrelated inline `CapabilityRequestCard` carries a `reason`) | MISSING — no suggestion surface, no "why" reason rendered in the grid, no per-suggestion install. |
|
||||
| **"N in this workspace" count bar (chips)** | a single render-time `"{visible.length} extensions"` text label `:277` | PARTIAL/WRONG semantics — it counts *visible filtered rows*, not *installed-in-this-workspace* capabilities, and is not a chip bar. |
|
||||
| **Category filter All/Skills/Connectors/MCP** | 7-facet rail All/Skills/Agents/Connectors/MCPs/Models/Templates | SUPERSET — current has the 3 design facets *plus* Agents/Models/Templates. Design shows 4 (All/Skills/Connectors/MCP). PR4 must decide: collapse to the design's 3-shelf framing or keep the 6-domain superset. |
|
||||
| **Card grid** (kind badge + name + desc + **install count** + Add button) | vertical *list* of cards; "kind badge" is a plain text `ext.type` pill; **no install count**; single "Install"/"Remove"/"Open in" action | PARTIAL — needs a real grid, a styled kind badge, an install-count field, and **type-aware** primary actions. |
|
||||
| **Type-aware one-click flows** (skill Add→Adding…→Added; connector Connect→Signing in…→Connected ~1.1s token→vault; MCP Enable→Enabling…→Enabled) | one generic "Install" label + single spinner; connectors/MCPs are *federated deep-links* (Open-in), not in-place actions | MISSING — no per-type verb, no progress→done micro-states, no in-place connect/enable; connector/MCP installs currently *leave* the Marketplace. |
|
||||
| **Toast + count-bar update per action** | install toasts fire; count bar does not track installed-count | PARTIAL — toasts ✓, count-bar-update ✗. |
|
||||
| **Shared "sync" store** (one store powers grid + agent-pick + inline card; install anywhere reflects everywhere) | grid keeps a local `extensions` array; inline card keeps its own `phase`; no shared store, no cross-reflect | MISSING — the headline architectural gap. |
|
||||
|
||||
### Variation B (inline) delta
|
||||
- `CapabilityRequestCard` exists but is **skill/marketplace-only** — no connector / MCP
|
||||
kinds. Needs the same type-aware verbs as the grid (Connect/Enable).
|
||||
- **No vault-aware approval** copy ("token goes to your vault"). The warm
|
||||
`InlineApprovalCard` (`components/os/warm/InlineApprovalCard.tsx`) is the obvious
|
||||
adoption target — it already renders `--honey-wash` + risk vocabulary + Approve/Not-now,
|
||||
and connectors carry credentials via `connectConnector(id, {token,…})`.
|
||||
- **No "connected follow-up"** state distinct from generic "Installed".
|
||||
|
||||
### Data availability for "install count"
|
||||
The marketplace DB schema **does** persist a `downloads` (and `stars`) column
|
||||
(`packages/server/src/local/routes/marketplace.ts:860`, `upsertPackage`), so an install
|
||||
count is *backed by data*. But it is **not surfaced**: the FE `MarketplacePackageRow`
|
||||
(`extension-catalog.ts:54-65`) omits `downloads`, `getMarketplace` returns
|
||||
`packages: unknown[]`, and `Extension` has no `installs` field. PR4 would thread
|
||||
`downloads` → `Extension.installCount` → `ExtensionCard`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Warm-Hive tokens / components to adopt
|
||||
|
||||
MarketplaceApp/ExtensionCard are still on the **legacy semantic-token** vocabulary
|
||||
(`bg-secondary/20`, `text-muted-foreground`, `border-border/30`, `var(--honey-500)`,
|
||||
`bg-primary`). The warm-Hive PR1+ vocabulary they should migrate to:
|
||||
|
||||
- **Tokens** (defined `apps/web/src/waggle-theme.css`, used across `warm/*`):
|
||||
`--honey`, `--honey-wash`, `--honey-line`, `--shadow-honey`, `--surface`, `--line`,
|
||||
`--line-strong`, `--text`, `--text-2`, `--text-dim`, `--attention`.
|
||||
- **`AskBar`** (`components/os/warm/AskBar.tsx`) — the warm full-width ask pill (honey "+",
|
||||
⌘K hint, honey send). This is the direct fit for the screen-09 "centered agent-search
|
||||
bar" (swap placeholder to "Describe what you want to do…", wire submit → agent
|
||||
suggestion instead of free chat).
|
||||
- **`InlineApprovalCard`** (`components/os/warm/InlineApprovalCard.tsx`) — the vault-aware
|
||||
honey approval card for Variation B's connector token→vault approval.
|
||||
- **`SectionLabel`**, **`IconTile`/`HexCheckTile`**, **`StatusBadge`**
|
||||
(`components/ui/status-badge.tsx`, already used) for kind badges / count chips.
|
||||
- **`StreakChip`/`RunChip`** patterns (`components/os/warm/`) as the visual idiom for the
|
||||
"N in this workspace" chip bar.
|
||||
- All warm atoms are barrel-exported from `apps/web/src/components/os/warm/index.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Exact integration points a PR4 build would touch
|
||||
|
||||
- **Grid host**: `apps/web/src/components/os/apps/MarketplaceApp.tsx` — `MarketplaceApp`
|
||||
component; `loadFacet` (fan-out/merge), `handleInstall`/`handleUninstall`/`handleOpenIn`,
|
||||
`buildInstallRequest`/`buildRemoveRequest`/`installRiskFor`.
|
||||
- **Card**: `apps/web/src/components/os/apps/extend/ExtensionCard.tsx` — `ExtensionCard`
|
||||
(add kind badge styling, install count, type-aware action verbs + micro-states).
|
||||
- **View-model + normalizers**: `apps/web/src/lib/extension-catalog.ts` — `Extension`
|
||||
interface (add `installCount`, an `installState` micro-phase, make connector/mcp
|
||||
`installable`), `fromConnector`/`fromMcpCatalogRow`/`fromMarketplacePackage`,
|
||||
`filterExtensions`, `sortExtensions`.
|
||||
- **Inline card**: `apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx`
|
||||
+ `capability-request-parser.ts` (`segmentText`, `CapabilityRequest`) — extend `kind`
|
||||
to `'connector' | 'mcp'`, wire `connectConnector`/`installMcp`, vault-aware approval.
|
||||
- **Shared store (NEW)**: nothing exists today. A PR4 "sync" store would be a new module
|
||||
(e.g. a context/zustand/event-bus in `apps/web/src/lib/` or `providers/`) that both
|
||||
`MarketplaceApp` and `CapabilityRequestCard` subscribe to; cross-reflect can also reuse
|
||||
the existing `window.dispatchEvent('waggle:*')` event idiom already used for
|
||||
`waggle:open-app` and `waggle:tier-insufficient`.
|
||||
- **Adapter (existing, reuse)**: `apps/web/src/lib/adapter.ts` — `getMarketplace`,
|
||||
`installMarketplacePackage`, `uninstallMarketplacePackage`, `searchMarketplace`,
|
||||
`getConnectors`, `connectConnector`, `getMcps`, `installMcp`, `getExtendAudit`.
|
||||
- **Types**: `packages/shared/src/types.ts` — `EXTENSION_TYPES` / `ExtensionType`
|
||||
(`:375-378`) is the canonical facet list; decide whether the design's 4-facet shelf
|
||||
collapses or wraps this 6-domain tuple.
|
||||
- **Server (read-only context)**: `packages/server/src/local/routes/marketplace.ts`
|
||||
(`/api/marketplace`, `/install`, `/uninstall`, `downloads` column),
|
||||
`packages/server/src/local/routes/extend.ts` (C18 audit).
|
||||
- **Route wrapper**: `apps/web/src/routes/MarketplaceRoute.tsx` (unchanged unless the shell
|
||||
framing changes).
|
||||
- **Tests to supersede**: `apps/web/src/test/phase4b-marketplace-extend.test.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Risks
|
||||
|
||||
- **Test churn**: 11 existing tests pin current testids and the `{type,limit:30}` call
|
||||
shape; a Variation-A rework rewrites most of them.
|
||||
- **Facet framing decision**: design says 3 shelves (Skills/Connectors/MCP); code has 6
|
||||
domains incl. Agents/Models/Templates with honest federated notes. Collapsing loses the
|
||||
honesty work; keeping diverges from the design's "one simple shelf".
|
||||
- **In-place connect/enable vs deep-link**: design wants connectors/MCPs installed *in the
|
||||
Marketplace* (Connect→Connected, Enable→Enabled). Today they are federated deep-links to
|
||||
Connector Hub / MCP Hub which own the real OAuth/security-scan/approval flows
|
||||
(`connectConnector`, `installMcp` w/ SecurityGate). Doing it in-place must NOT bypass
|
||||
those flows — risk of duplicating or weakening security gating.
|
||||
- **"Sync" consistency**: a shared store must reconcile optimistic flips with server truth
|
||||
(install can 403/securitygate-block after the optimistic flip) across both surfaces.
|
||||
- **Install count truthfulness**: `downloads` is local-single-user and often 0; rendering
|
||||
it as a social "install count" may be misleading (mind-isolation / honest-stats ethos
|
||||
the repo enforces elsewhere).
|
||||
|
||||
---
|
||||
|
||||
## 6. Open questions for the lead
|
||||
|
||||
1. **Facets**: collapse to the design's All/Skills/Connectors/MCP (3 shelves), or keep the
|
||||
6-domain superset (Agents/Models/Templates) with federated notes?
|
||||
2. **In-place vs deep-link** for connector/MCP: do Connect/Enable happen *inside*
|
||||
Marketplace (new in-place flow that must reuse the Hub security/OAuth paths), or stay
|
||||
deep-links? The design clearly wants in-place.
|
||||
3. **Shared "sync" store** mechanism: new context/store module vs reuse the existing
|
||||
`window` CustomEvent bus? What's the source of truth (server re-read vs optimistic +
|
||||
reconcile)?
|
||||
4. **"N in this workspace"**: counts *installed* capabilities (which backend gives the
|
||||
per-workspace installed set?) — is `getExtendAudit` / a capabilities-status read the
|
||||
source, or a new `/api/marketplace?installed=true&workspace=…`?
|
||||
5. **Install count**: surface `downloads` honestly, or omit it given local-single-user
|
||||
data is ~0 and the repo's honest-stats ethos?
|
||||
6. **Agent-suggestion box**: which backend produces the connector+skill+tool
|
||||
recommendation with a "why"? Is there an agent/LLM route to call, or is this a curated
|
||||
heuristic over the registry?
|
||||
7. **Inline Variation B**: extend `CapabilityRequestCard` in place, or replace it with a
|
||||
shared picker component used by both grid and chat?
|
||||
183
docs/redesign-warm-hive/pr4-recon/02-marketplace-backend.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# PR4 Recon — Slice 2: Marketplace Backend + Routes
|
||||
|
||||
**Design contract:** SCREENS.md §09 (Marketplace — skills + connectors + MCP as one shelf, agent-searchable).
|
||||
**PR4 scope (BUILD-PLAN.md §6, line 142):** "Marketplace + **shared install store ('sync')** (grid + agent-pick + inline-in-chat)" → `MarketplaceApp`, **new install store**, screen 09.
|
||||
**Branch:** `feature/warm-hive-pr4`. **Mode:** READ ONLY (no source touched).
|
||||
|
||||
This doc maps the *existing* backend territory a PR4 "shared install store (sync)" would build ON. It does not propose the plan.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR for the synthesizer
|
||||
|
||||
There is **no single install store today**. The §09 contract assumes *one* store powering grid + agent-pick + inline-card, with type-aware micro-states (skill Add→Adding→Added, connector Connect→Signing in→Connected token→vault, MCP Enable→Enabling→Enabled) and a workspace-scoped "N in this workspace" count bar. The actual backend splits install state across **three independent persistence layers**, none of which agree on a shared shape, and **none of which are workspace-scoped**:
|
||||
|
||||
| Kind | "Is it installed?" source of truth | Install verb / route | Workspace-scoped? |
|
||||
|---|---|---|---|
|
||||
| **skill** / **plugin** / **mcp** (marketplace pkg) | `installations` table in `marketplace.db` (`status='installed'`), keyed by **numeric `package_id`** | `POST /api/marketplace/install` | **No** — global |
|
||||
| **mcp** (runtime) | `<dataDir>/.mcp.json` ⋈ live `McpRuntime` server states, keyed by **string server name** | `POST /api/mcps/install` (delegates to marketplace) | **Partial** — a server carries one optional `workspaceId` tag (C19), but install presence is global |
|
||||
| **connector** | **Vault** credential under `connector:{id}` (presence = "connected"), keyed by **string connector id** | `POST /api/connectors/:id/connect` (token→vault) | **No** — vault is single-user/global |
|
||||
|
||||
The install-audit trail (`install_audit` table) **is the only thing that already unifies all three** as a write-side feed — but it is an append-only *log*, not a queryable "installed set," and it has **no `workspace_id` column** (schema.ts:135-152). So the count-bar's "N in this workspace" claim has **no backing data today**; it would either need a new scoping column or be redefined as a global count.
|
||||
|
||||
The current FE (`MarketplaceApp.tsx`) already does federate-at-read across 6 domains but holds install state only in **local React `useState`** — installing patches `extensions[]` in place; there is no cross-view store, so "installing in any view reflects in all" (the §09 CRITICAL line) is **not satisfied** today.
|
||||
|
||||
---
|
||||
|
||||
## 1. Marketplace DB schema (`packages/marketplace/src/db.ts` + types.ts)
|
||||
|
||||
`MarketplaceDB` wraps a `better-sqlite3` handle at `~/.waggle/marketplace.db` (overridable; the server opens `<dataDir>/marketplace.db`, seeded by copy from `packages/marketplace/marketplace.db` — server/local/index.ts:457-506). WAL mode, FK on.
|
||||
|
||||
### Tables (inferred from queries — there is **no `CREATE TABLE` DDL in the TS source**; the schema ships pre-built inside the committed `marketplace.db` seed file)
|
||||
|
||||
- **`packages`** — the catalog. Columns referenced in code (db.ts, types.ts `MarketplacePackage`): `id` (PK), `source_id` (FK→sources), `name`, `display_name`, `description`, `author`, `package_type` (`skill|plugin|mcp_server|template|pack`), **`waggle_install_type`** (`skill|plugin|mcp` — the dispatch discriminant), `waggle_install_path`, `version`, `license`, `repository_url`, `homepage_url`, `downloads`, `stars`, `rating`, `rating_count`, `category`, `subcategory`, `install_manifest` (JSON), `platforms` (JSON), `min_waggle_version`, `dependencies` (JSON), `packs` (JSON), `created_at`, `updated_at`. **Plus security columns** added by the installer's `recordScanResult()` (types.ts:81-90 `PackageSecurityColumns`): `security_status`, `security_score`, `last_scanned_at`, `content_hash`, `scan_engines`, `scan_findings`, `scan_blocked`.
|
||||
- **`packages_fts`** — FTS5 virtual table joined on `p.id = fts.rowid` for full-text search (db.ts:103). `rank` column used for relevance ordering.
|
||||
- **`sources`** — catalog provenance. Columns (db.ts:271-283, types.ts `MarketplaceSource`): `id`, `name`, `display_name`, `url`, `source_type`, `platform`, `total_packages`, `install_method`, `api_endpoint`, `description`, `last_synced_at`, **`is_custom`** (auto-migrated, db.ts:57-67), **`sync_state`** (auto-migrated JSON for resumable sync, db.ts:69-78).
|
||||
- **`packs`** — capability bundles. Columns (types.ts `MarketplacePack`): `id`, `slug`, `display_name`, `description`, `target_roles`, `icon`, `priority` (`core|recommended|optional`), `connectors_needed` (JSON), `created_at`.
|
||||
- **`pack_packages`** — pack↔package join with `is_core` flag (db.ts:191-194).
|
||||
- **`installations`** — **THE install-state source of truth for marketplace packages** (db.ts:350-404). Columns (types.ts `Installation`): `id`, `package_id` (FK), `installed_version`, `installed_at`, `install_path`, `status` (`installed|updating|failed|uninstalled`), `config` (JSON — secret VALUES redacted to `[redacted]`, keys kept; installer.ts:164-173). **No `workspace_id`.**
|
||||
- **`scan_history`** — append-only security scan log (installer.ts:644-659): `package_id`, `scanned_at`, `overall_severity`, `security_score`, `content_hash`, `engines_used`, `findings`, `blocked`, `scan_duration_ms`, `triggered_by`.
|
||||
|
||||
### Install-state API on `MarketplaceDB` (the methods a sync store would call)
|
||||
|
||||
- `recordInstallation(packageId, version, installPath, config)` → inserts `installations` row, `status='installed'` (db.ts:350).
|
||||
- `isInstalled(packageId): boolean` → `SELECT 1 FROM installations WHERE package_id=? AND status='installed'` (db.ts:390). **This is the per-package installed check.**
|
||||
- `markUninstalled(packageId)` → flips status to `'uninstalled'` (db.ts:400).
|
||||
- `listInstallations(): InstalledPackageRow[]` → joins `installations` ⋈ `packages` for `status='installed'`, ordered by `installed_at DESC` (db.ts:376).
|
||||
- `getInstalledCount(): number` → **global** count of `status='installed'` rows (db.ts:500). This is the only existing "count bar" primitive — **not workspace-aware**.
|
||||
- `getPackage(id)` / `getPackageByName(name)` — by numeric id / string name.
|
||||
- `search(SearchOptions): SearchResult` — FTS5 + faceted filters; returns `{ packages, total, facets:{types,categories,sources}, installedCount }` (db.ts:86-165). `installedCount` here is the global `getInstalledCount()`.
|
||||
|
||||
---
|
||||
|
||||
## 2. List/search/install/uninstall APIs (`packages/server/src/local/routes/marketplace.ts`)
|
||||
|
||||
Registered as `marketplaceRoutes(fastify)` (server/local/index.ts:95). Every route gates on `fastify.marketplace` (the decorated `MarketplaceDB|null`, index.ts:506); `requireDb()` returns **503** if absent.
|
||||
|
||||
| Method · Route | Purpose | Tier gate | Notes |
|
||||
|---|---|---|---|
|
||||
| `GET /api/marketplace/search` | FTS5 + facet search of the catalog | none | Annotates each pkg with `installed: db.isInstalled(pkg.id)`, `scanStatus`, `scanScore`; appends `categories: PACKAGE_CATEGORIES` (marketplace.ts:57-118). **This is the grid's read.** |
|
||||
| `GET /api/marketplace` (bare alias) | S21 alias → injects into `/search`; accepts 6-domain `type` facet; returns honest empty `{federated:true}` for non-marketplace domains | none | `extend.ts:49-85`. **This is what the current FE `adapter.getMarketplace()` calls.** |
|
||||
| `GET /api/marketplace/packs` · `/packs/:slug` | List packs / pack detail | none | marketplace.ts:123-146 |
|
||||
| `GET /api/marketplace/enterprise-packs` | KVARK-gated packs | `requireTier('ENTERPRISE')` | marketplace.ts:152 |
|
||||
| **`POST /api/marketplace/install`** | **Install a package by numeric `packageId`** | `requireTier('PRO')` | marketplace.ts:181-432. SecurityGate pre-scan (heuristics-only) → severity gating (CRITICAL=403 always; HIGH=403 unless `force`; MEDIUM/LOW proceed) → `MarketplaceInstaller.install()` → audit rows. Returns `{...InstallResult, security:{...}}`, **200** on success / **422** on fail / **403** on block. |
|
||||
| `POST /api/marketplace/uninstall` | Uninstall by `packageId` | none | marketplace.ts:437-470. For MCP packages ALSO tears down the live runtime + `<dataDir>/.mcp.json` (else it resurrects at boot). |
|
||||
| `GET /api/marketplace/installed` | `listInstallations()` | none | marketplace.ts:475 |
|
||||
| `POST /api/marketplace/security-check` | Scan-only by id | none | marketplace.ts:486 |
|
||||
| `GET/POST/DELETE /api/marketplace/sources` | Source CRUD + sync-on-add | none | marketplace.ts:525-641 |
|
||||
| `GET /api/marketplace/categories` | `PACKAGE_CATEGORIES` taxonomy (21 cats) | none | marketplace.ts:646 — feeds the category filter |
|
||||
| `POST /api/marketplace/sync` | Manual sync from sources | none | marketplace.ts:655 |
|
||||
| `GET /api/marketplace/security-status` | Cisco scanner availability + aggregate scan counts | none | marketplace.ts:712 |
|
||||
| `POST /api/marketplace/publish` | Publish a local skill into the catalog | `requireTier('PRO')` | marketplace.ts:764 |
|
||||
|
||||
**Tier note for the §09 "one-click install":** `POST /api/marketplace/install` is **PRO-gated**. The skill "Add (instant)" micro-state in the contract collides with PRO-gating unless the install path for FREE-tier skills differs. The MCP path (`/api/mcps/install`) is also PRO. Connector connect (`/api/connectors/:id/connect`) is **ungated**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Installer flow per type (`packages/marketplace/src/installer.ts`)
|
||||
|
||||
`MarketplaceInstaller.install(request: InstallRequest)` (installer.ts:81) is the **single dispatch entrypoint** for skill/plugin/mcp. Flow:
|
||||
1. `db.getPackage(packageId)` → 404-shaped result if missing.
|
||||
2. Idempotency: if `!force && db.isInstalled(pkg.id)` → returns success "already installed" (installer.ts:96).
|
||||
3. **Security gate** (`SecurityGate.scan`) on resolved content → `recordScanResult()` → if `blocked && !forceInsecure` returns blocked result (installer.ts:116-130).
|
||||
4. **Dispatch on `pkg.waggle_install_type`** (installer.ts:137):
|
||||
- **`skill`** → `installSkill()` (installer.ts:309): writes `~/.waggle/skills/{name}.md` from `manifest.skill_content` / `skill_url` / repo `SKILL.md` / generated stub; then `PUT /api/skills/{name}` notify. **Instant** (matches §09 "Add→Added instant").
|
||||
- **`plugin`** → `installPlugin()` (installer.ts:367): mkdir `~/.waggle/plugins/{name}/`, git-clone or `npm install`, write `plugin.json`, install bundled skills, update `registry.json`, run post-install hooks, `POST /api/plugins/install` notify. Slow / multi-step.
|
||||
- **`mcp`** → `installMcp()` (installer.ts:489): optional `npm install -g`, apply settings to env, write the server entry into `<dataDir>/.mcp.json` (`mcpConfigPath()` = `WAGGLE_DATA_DIR/.mcp.json`, installer.ts:47). **Does NOT start the runtime** — that is the `/api/mcps/install` route's job (mcps.ts:277-294). (§09 "Enable→Enabling→Enabled" = install + runtime start.)
|
||||
5. On success: `db.recordInstallation()` (with redacted setting keys) and attaches `scanResult`.
|
||||
|
||||
`uninstall(packageId)` (installer.ts:256) mirror-dispatches: `uninstallSkill` (rm file + DELETE notify), `uninstallPlugin` (rmdir + registry + DELETE), `uninstallMcp` (remove from `.mcp.json`), then `db.markUninstalled()`.
|
||||
|
||||
**Notify pattern:** the installer best-effort POSTs to `API_BASE` (`WAGGLE_API_URL` || `http://localhost:3000`, installer.ts:52) — fire-and-forget, swallows failure (installer.ts:764). Note the default port 3000 vs the running dev sidecar; relevant if a sync store relied on the notify hook firing.
|
||||
|
||||
---
|
||||
|
||||
## 4. The three install-state stores in detail (the "sync" problem)
|
||||
|
||||
The §09 CRITICAL line — *"one store powers the grid, the agent picks, AND the inline card … installing in any view reflects in all"* — has no backend equivalent. The three stores:
|
||||
|
||||
### 4a. Marketplace packages (skill/plugin/mcp) — `installations` table
|
||||
- Truth: `MarketplaceDB.isInstalled(packageId: number)`. Keyed by **numeric package id**.
|
||||
- Read surfaces: `/api/marketplace/search` (annotates `installed`), `/api/marketplace/installed`.
|
||||
|
||||
### 4b. MCP runtime — `<dataDir>/.mcp.json` ⋈ `McpRuntime` (`routes/mcps.ts`)
|
||||
- Truth: `GET /api/mcps` (mcps.ts:143) = `MCP_CATALOG` (from `@waggle/shared`) ⋈ persisted `.mcp.json` entries ⋈ live `runtime.getServerStates()`. Keyed by **string server name**. `installed = name ∈ (persisted ∪ runtime)`.
|
||||
- Statuses: `installed | running | error | stopped` (mcps.ts:67 `toInstanceStatus`).
|
||||
- `POST /api/mcps/install` (mcps.ts:189) **delegates to `/api/marketplace/install`** via `fastify.inject`, then registers + `start()`s the runtime server (8s budget). So an MCP "install" touches BOTH 4a and 4b. **This is the closest thing to a working cross-store sync** — but it is MCP-specific and one-directional.
|
||||
- **Workspace tag:** `PATCH /api/mcps/:id/permissions` sets a single `workspaceId` (C19 — "single-workspace scoping v1", mcps.ts:535). This is the ONLY place a "workspace" appears in install state, and it is a *scope filter on tool exposure*, not an install-presence scoping.
|
||||
|
||||
### 4c. Connectors — Vault (`routes/connectors.ts`)
|
||||
- Truth: presence of a vault credential under `connector:{id}` (`fastify.vault.getConnectorCredential(id)`). "Connected" = credential exists & not expired.
|
||||
- Read: `GET /api/connectors` → `connectorRegistry.getDefinitions()` (a STATIC catalog of all connectors) + `GET /api/connectors/:id/health` for live `connected|disconnected|expired|error` status.
|
||||
- Install verb: `POST /api/connectors/:id/connect` — stores token→vault (matches §09 "token → vault"), re-inits the connector, writes audit. **Ungated.** Disconnect/revoke purge vault + OAuth tokens.
|
||||
- **Connectors are NOT in `marketplace.db` at all.** They never appear in `installations`/`isInstalled`. The §09 grid showing connectors alongside skills/MCP must federate from `/api/connectors`.
|
||||
|
||||
### 4d. The unifying write-side feed — `install_audit` (the one cross-type thing that exists)
|
||||
- `fastify.auditStore` (`@waggle/core` `install-audit.ts`) writes to table `install_audit` (schema.ts:135-152). All three stores already write here on install/uninstall/connect/revoke (marketplace.ts, mcps.ts `recordMcpAudit`, connectors.ts `recordConnectorAudit`).
|
||||
- **Read:** `GET /api/extend/audit` (extend.ts:88) — ONE shared feed across `native|skill|plugin|mcp|connector|marketplace` with `?type=` / `?capability=` filters. The `AuditStore` API: `getRecent(limit)`, `getRecentByType(type, limit)`, `getByCapability(name)`.
|
||||
- **CRITICAL GAP for the count bar:** `install_audit` has **NO `workspace_id` column** (schema.ts:135-152 — confirmed; `ai_interactions`/`execution_traces` DO have it at schema.ts:174/219, install_audit does not). It is an append-only event log, not a "current installed set." A "N in this workspace" count therefore has **zero backing data** today.
|
||||
|
||||
---
|
||||
|
||||
## 5. The install-audit trail (what's already auditable)
|
||||
|
||||
`RecordAuditInput` fields (used by all three route files): `capabilityName`, `capabilityType` (`native|skill|plugin|mcp|connector|marketplace`), `source`, `riskLevel` (`low|medium|high|critical`), `trustSource` (`builtin|starter_pack|local_user|third_party_verified|third_party_unverified|unknown|security-gate`), `approvalClass` (`standard|elevated|critical|blocked`), `action` (`proposed|approved|installed|rejected|failed|blocked|uninstalled`), `initiator` (`agent|user|system`), `detail`. CHECK constraints in schema.ts:143-150 enforce these — **adding a value (e.g. a `synced` action or a `workspace_id`) is a migration + CHECK change**, and the file warns these drifted once and crashed `acquire_capability` (schema.ts:139-142). Treat the audit vocabulary as a contract.
|
||||
|
||||
The marketplace install route writes audit rows for CRITICAL-block / HIGH-block / HIGH-force-override / MEDIUM / LOW / blocked-by-installer / forceInsecure-override (marketplace.ts:222-415) — a thorough trust trail, all global.
|
||||
|
||||
---
|
||||
|
||||
## 6. Exact integration points a PR4 "sync store" would hook into
|
||||
|
||||
**Reads (catalog + installed-state):**
|
||||
- `GET /api/marketplace/search` (grid; annotates `installed` per pkg) — or the bare `GET /api/marketplace?type=` alias the FE already uses.
|
||||
- `GET /api/marketplace/installed` → `MarketplaceDB.listInstallations()`.
|
||||
- `GET /api/mcps` (catalog ⋈ persisted ⋈ runtime; `installed` + `status`).
|
||||
- `GET /api/connectors` + `GET /api/connectors/:id/health` (connector connected-state).
|
||||
- `GET /api/marketplace/categories` (the All/Skills/Connectors/MCP filter taxonomy).
|
||||
- `GET /api/extend/audit` (the existing cross-type unified feed — the natural read for a "what's installed across everything" view, modulo it being a log).
|
||||
|
||||
**Install verbs (the three one-click flows):**
|
||||
- skill / plugin → `POST /api/marketplace/install` `{ packageId }` (PRO). FE adapter: `adapter.installMarketplacePackage(packageId)` → `/api/marketplace/install` (adapter.ts:1339).
|
||||
- mcp → `POST /api/mcps/install` `{ mcpId, settings, force }` (PRO) — delegates to marketplace install + runtime start; uninstall via `POST /api/mcps/:id/revoke`.
|
||||
- connector → `POST /api/connectors/:id/connect` `{ token|apiKey }` (token→vault); disconnect `POST /api/connectors/:id/disconnect`, strong revoke `POST /api/connectors/:id/revoke`.
|
||||
|
||||
**Installer functions (package layer, if the store goes below the routes):**
|
||||
- `MarketplaceInstaller.install(InstallRequest)` / `.uninstall(packageId)` / `.installPack(slug)` (installer.ts:81/256/210).
|
||||
- `MarketplaceDB.isInstalled(id)` / `recordInstallation(...)` / `markUninstalled(id)` / `getInstalledCount()` / `listInstallations()` (db.ts).
|
||||
|
||||
**Server decoration:** `fastify.marketplace: MarketplaceDB | null` (index.ts:506); `fastify.connectorRegistry`, `fastify.vault`, `fastify.auditStore`, `fastify.agentState.mcpRuntime` are the sibling singletons a unified store would coordinate.
|
||||
|
||||
**Current FE consumer to refactor:** `apps/web/src/components/os/apps/MarketplaceApp.tsx` (417 LOC) — already federates 6 domains at read (skill via `getMarketplace`, packs, mcp via `getMcps`+`getMarketplace`, persona, connector via `getConnectors`, model, template) but holds install state in **local `useState extensions[]`**; install just patches the row (`installed: true`) — **no shared store, no cross-view propagation.** View-model federation already lives in `apps/web/src/lib/extension-catalog.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Key deltas vs the §09 contract (territory, not plan)
|
||||
|
||||
1. **No single install store.** Three stores (marketplace `installations` / `.mcp.json`+runtime / vault), three key types (numeric id / string name / string id), three install verbs, three tiers (PRO / PRO / ungated). The §09 "one store … reflects in all" is the central build.
|
||||
2. **No workspace scoping of install state.** `installations` and `install_audit` have no `workspace_id`; vault is global; only MCP carries a single optional `workspaceId` *tool-scope* tag. The "N in this workspace" count bar has no data source — needs either a new scoping dimension or redefinition to a global count.
|
||||
3. **No agent-suggestion endpoint.** The §09 "agent-suggestion box (connector + skill + tool, each with a why)" has no backing route. FTS search (`/api/marketplace/search`) + the agent's `acquire_capability` path (the verbose-`need` FTS handling in db.ts:91-99 / `toFtsMatchQuery`) is the nearest substrate, but a "recommend one of each kind with a reason" composite does not exist.
|
||||
4. **No inline-in-chat picker route.** Variation B (mid-conversation connector offer with vault-aware approval) reuses the same install verbs but has no dedicated surfacing/approval endpoint; the connector connect flow + the existing approval/confirmation machinery (`routes/approval.ts`, agent `confirmation.ts`) are the substrate.
|
||||
5. **Connectors absent from `marketplace.db`.** Any unified grid must federate connectors from `/api/connectors`, not from the marketplace catalog — they share no row shape with `MarketplacePackage`.
|
||||
6. **Audit feed is a log, not a set.** `GET /api/extend/audit` unifies *events* across types but cannot answer "what is currently installed in workspace W" without scanning + reducing; the count bar wants a live set.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks / sharp edges for a builder
|
||||
|
||||
- **Schema DDL is invisible.** The `packages`/`sources`/`packs`/`installations`/`scan_history` tables exist only inside the committed `marketplace.db` seed binary — there is no `CREATE TABLE` in TS. Adding a `workspace_id` to `installations` means a runtime `ALTER TABLE` migration in `MarketplaceDB.migrateSchema()` (the established pattern, db.ts:57-78), not an edit to a schema file.
|
||||
- **`install_audit` CHECK constraints are a hard contract.** Adding a `synced` action or any new vocabulary needs a coordinated `runMigrations()` + CHECK rewrite; schema.ts explicitly records a prior drift that crashed `acquire_capability`.
|
||||
- **PRO-gating vs "instant Add".** Skill install is PRO-gated server-side; the §09 instant micro-state must reconcile with `requireTier('PRO')` (or rely on a different non-gated skill path).
|
||||
- **MCP install is the only working cross-store coupling** (`/api/mcps/install` writes both `installations` and `.mcp.json`+runtime, and revoke keeps marketplace `installed` honest, mcps.ts:509-519). A unified store should mirror this bidirectional bookkeeping for the other types, or it will drift (the FE will show "installed" after a revoke, etc.).
|
||||
- **OSS-sync constraint (§7.5):** `install_audit` DDL inside `mind/schema.ts` is OSS-excluded (Waggle governance). A `workspace_id` addition there has nowhere to land on the public mirror — fine for the monorepo, but flag it as a curated-strip item.
|
||||
- **Three-store consistency under failure.** The installer's `notify` hook is fire-and-forget to `localhost:3000` (not the running sidecar port); a sync store cannot rely on the notify callback to invalidate caches.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open questions for the founder/lead
|
||||
|
||||
1. **Workspace scoping:** Is "N in this workspace" a real per-workspace install set (requires a new `workspace_id` on installs + per-workspace activation model), or a relabeled global count for v1? This is the single biggest decision — it determines whether the sync store needs a new data dimension across all three backends.
|
||||
2. **Skill install tier:** Should the §09 instant skill "Add" stay PRO-gated, or is there a FREE skill-install lane? (CLAUDE.md moat: skills/connectors are the upgrade trigger — so PRO-gating may be intentional and the "Add" CTA should show the upgrade nudge instead of installing.)
|
||||
3. **Agent-suggestion box:** Build a new composite recommend endpoint (one connector + one skill + one tool + "why"), or compose it client-side from three `search` calls + an LLM rationale? No backing route exists either way.
|
||||
4. **Sync store location:** A FE-only store (React context / Zustand over the existing routes) vs a new server-side unified `/api/install-state` read that reduces all three stores into one shape. The §09 "one store reflects in all" is achievable purely client-side IF every view subscribes to it; a server read is only needed for the count bar's correctness across reloads.
|
||||
5. **Inline-in-chat (Variation B):** Reuse `/api/connectors/:id/connect` + existing approval machinery, or a dedicated in-chat install/approval contract? Decide before building the chat surface.
|
||||
370
docs/redesign-warm-hive/pr4-recon/03-install-state-sync.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# PR4 Recon · Slice 3 — Shared Install State ("sync")
|
||||
|
||||
**Branch:** `feature/warm-hive-pr4` · **Screen:** 09 Marketplace · **Read-only recon.**
|
||||
**Design contract:** `docs/design_handoff_waggle_app/SCREENS.md` §09 (lines 189-207) +
|
||||
the reference implementation in `docs/design_handoff_waggle_app/design-files/screens/marketplace.html`
|
||||
(lines 203-310 — the canonical `installed`/`installing` Set + `renderAll()` "sync" model).
|
||||
|
||||
> This is THE critical design element of PR4: "one store powers the grid, the agent picks,
|
||||
> AND the inline card. Installing in any view reflects in all of them."
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR — the delta in one paragraph
|
||||
|
||||
There is **no shared install state today.** Three install backends exist and are battle-tested,
|
||||
but each lives in its own server module with its **own persistence mechanism and its own
|
||||
notion of "installed"**: skills = markdown files on disk reloaded into `agentState.skills`;
|
||||
connectors = vault credentials; MCP = `.mcp.json` + a live runtime. The FE has **no unified
|
||||
store** — `MarketplaceApp` (the existing Phase-4B Extend surface) federates the six facets
|
||||
**at read** into a local `useState<Extension[]>` and **discards that view on unmount**. Installing
|
||||
in the grid does NOT reflect in chat's `CapabilityRequestCard` (which keeps its own local
|
||||
`phase` state) and vice-versa. There is **no count bar**, **no type-aware Add/Connect/Enable
|
||||
verbs**, and **no progress→done micro-states** on the cards. The single cross-type fact that
|
||||
IS already shared is the **install-audit trail** (`server.auditStore`, the `install_audit`
|
||||
table) — every install path writes to it, but it is an append-only event log, not a queryable
|
||||
"installed set." PR4 must introduce a real FE store + a thin server aggregate so the three
|
||||
backends present as ONE reactive installed set.
|
||||
|
||||
---
|
||||
|
||||
## 1. Where install state lives TODAY — per type
|
||||
|
||||
### 1a. Skills — filesystem-backed
|
||||
**Server:** `packages/server/src/local/routes/skills.ts`
|
||||
- **Source of truth:** `.md` files in `<dataDir>/skills/` (default `~/.waggle/skills/`).
|
||||
"Installed" = file exists on disk; "active" = also loaded into `server.agentState.skills`.
|
||||
- **Install:** copies starter/pack `.md` into `skillsDir`, then reloads
|
||||
`server.agentState.skills.length = 0; push(...loadSkills(waggleHome))` (skills.ts:204-205,
|
||||
333-334). Authored skills go through `writeSkill(...)` (the P5/D4 shared write-service that
|
||||
redacts + stamps provenance + audits) at skills.ts:439, 500, 545.
|
||||
- **Routes:**
|
||||
- `POST /api/skills/starter-pack/:id` (skills.ts:168) — single starter skill, instant
|
||||
- `POST /api/skills/capability-packs/:id` (skills.ts:271) — install a whole pack
|
||||
- `POST /api/skills/:id/install` — alias in `skills-aliases.ts` (the one the FE adapter
|
||||
`installSkill(id, source, packageId?)` actually calls — adapter.ts:1240-1256)
|
||||
- `DELETE /api/skills/:name` (skills.ts:563) — uninstall (audits `'uninstalled'`)
|
||||
- `GET /api/skills` (skills.ts:345) — list installed (with provenance/initiator)
|
||||
- `GET /api/skills/capability-packs/catalog` (skills.ts:238) — packs with per-skill
|
||||
`state: 'active'|'installed'|'available'` and a `packState`/`installedCount`/`totalCount`
|
||||
- **Audit:** `server.auditStore.record({ capabilityType: 'skill', action: 'installed'|'uninstalled', ... })`
|
||||
- **Micro-state in design:** `skill` → `Add → Adding… → Added` (instant; ~480ms in the mock).
|
||||
|
||||
### 1b. Connectors — vault-credential-backed
|
||||
**Server:** `packages/server/src/local/routes/connectors.ts`
|
||||
- **Source of truth:** the **Vault** (`packages/core/src/vault.ts`). "Installed/connected" =
|
||||
a credential exists under `connector:{id}` (`fastify.vault.setConnectorCredential`,
|
||||
connectors.ts:122). Status is derived **live** by `connectorRegistry.healthCheck(id)` /
|
||||
credential presence — there is no stored "installed" boolean.
|
||||
- **Connect (the design's "token → vault"):** `POST /api/connectors/:id/connect`
|
||||
(connectors.ts:96) stores the credential in the vault, re-inits the connector, and audits
|
||||
`action:'installed', trustSource:'local_user'`. OAuth tokens are also written by
|
||||
`oauth.ts` under `${provider}_oauth_token` (provider-keyed, NOT connector-keyed — see the
|
||||
`OAUTH_PROVIDER_FOR_CONNECTOR` Google-family map at connectors.ts:16-23).
|
||||
- **FE connect flow (the real vault path):** `ConnectorsApp.tsx` →
|
||||
`adapter.addVaultSecret({ key: 'connector:{id}', value: token, type:'bearer' })` THEN
|
||||
`adapter.connectConnector(id)` (ConnectorsApp.tsx:154-155).
|
||||
- **Routes:** `GET /api/connectors` (list+status, connectors.ts:38), `/connect`, `/disconnect`,
|
||||
`/sync` (C16 health re-probe + `lastSyncAt` stamp), `/revoke` (C17 strong purge incl. OAuth).
|
||||
- **Adapter:** `getConnectors()` (adapter.ts:1958), `connectConnector(id, creds?)` (1971),
|
||||
`disconnectConnector` (1981), `syncConnector` (1986), `revokeConnector` (1994).
|
||||
- **Micro-state in design:** `connector` → `Connect → Signing in… → Connected` (~1.1s, the
|
||||
"token goes to your vault" approval). **This is the only flow with an approval gate** in
|
||||
Variation B (the vault-aware "Add & connect" approval, marketplace.html:179-183).
|
||||
|
||||
### 1c. MCP — `.mcp.json` + live runtime
|
||||
**Server:** `packages/server/src/local/routes/mcps.ts`
|
||||
- **Source of truth:** persisted `<dataDir>/.mcp.json` (`mcp-config.ts`) **⋈** the live
|
||||
`McpRuntime` server states (`fastify.agentState.mcpRuntime`). "Installed" = present in
|
||||
`.mcp.json` OR registered in the runtime (mcps.ts:147). The C4 boot loader re-registers
|
||||
persisted entries so installs survive restarts.
|
||||
- **Install:** `POST /api/mcps/install` (mcps.ts:189, **PRO-gated** via `requireTier('PRO')`)
|
||||
**delegates to the marketplace installer** (`fastify.inject('/api/marketplace/install')`) so
|
||||
SecurityGate + scan + install_audit ride along, then `saveMcpServerEntry` + `runtime.addServer`
|
||||
+ `instance.start()`. Custom servers: `POST /api/mcps` (mcps.ts:331, also PRO-gated).
|
||||
- **Enable/disable:** `POST /api/mcps/:id/start` (mcps.ts:471), `/stop` (487), `/revoke` (496,
|
||||
removes from runtime + config + marks the marketplace package uninstalled).
|
||||
- **Routes/list:** `GET /api/mcps` (mcps.ts:143) returns `{ mcps, total, installed }` where
|
||||
each row carries `installed`, `status`, `scope`, `state`, `tools`.
|
||||
- **Adapter:** `getMcps()` (adapter.ts:2004), `installMcp(mcpId, opts)` (2017),
|
||||
`addCustomMcp` (2031), `testMcp` (2045).
|
||||
- **Micro-state in design:** `mcp` → `Enable → Enabling… → Enabled` (~720ms in the mock).
|
||||
|
||||
### 1d. Marketplace registry (the spine the others lean on)
|
||||
**Server:** `packages/server/src/local/routes/marketplace.ts` (uses `@waggle/marketplace`
|
||||
`MarketplaceDB`/`MarketplaceInstaller`/`SecurityGate`). The `packages` table is SQLite; a
|
||||
package's domain is `waggle_install_type` (`'skill'|'mcp'|'plugin'`), NOT a `type` column.
|
||||
- `GET /api/marketplace/search` (`installed: db.isInstalled(pkg.id)`, marketplace.ts:107)
|
||||
- `POST /api/marketplace/install` (marketplace.ts:181, PRO-gated; SecurityGate + audit)
|
||||
- `POST /api/marketplace/uninstall` (marketplace.ts:437; MCP cleanup ride-along)
|
||||
- `GET /api/marketplace/installed` (marketplace.ts:475 → `db.listInstallations()`)
|
||||
- `GET /api/marketplace/packs` (raw `MarketplacePack` rows — **no install route exists**, so
|
||||
packs are browse-only today; see `fromSkillPack` `installable:false`)
|
||||
- **Adapter:** `getMarketplace({query,type,limit})` (adapter.ts:2082), `searchMarketplace`
|
||||
(1334), `installMarketplacePackage(packageId)` (1338, returns raw Response),
|
||||
`uninstallMarketplacePackage` (1345), `getMarketplacePacks` (1309).
|
||||
|
||||
---
|
||||
|
||||
## 2. The ONLY thing shared today: the install-audit trail
|
||||
|
||||
`server.auditStore` (the `install_audit` table) is the single cross-type surface every install
|
||||
path already writes to:
|
||||
- skills: skills.ts:212, 315 + the `writeSkill`/`deleteSkill` service
|
||||
- connectors: connectors.ts `recordConnectorAudit` (29) on connect/sync/revoke
|
||||
- mcp: mcps.ts `recordMcpAudit` (93) on install/custom-add/revoke
|
||||
- marketplace: writes its own rows on non-clean scans
|
||||
|
||||
It is exposed as `GET /api/audit/installs` (skills.ts:718) and a shared Extend feed
|
||||
`GET /api/extend/audit?type=…` (rendered by `extend/InstallAuditPanel.tsx`, used as the
|
||||
Marketplace "Audit" tab). **But it is an append-only EVENT log, not a queryable installed-set**
|
||||
— you cannot ask it "what is installed right now" without replaying install/uninstall pairs.
|
||||
`install_audit` is also **OSS-EXCLUDED** (CLAUDE.md §7.5) — fine for a Waggle-only count, but it
|
||||
must NOT become the substrate's source of truth.
|
||||
|
||||
> **No skill_share/diffusion wiring lives in the server `local/` layer.** `lifecycle.ts` has
|
||||
> none; the `onSkillDistillationFire → skill_share` callback is the agent layer (CLAUDE.md §10
|
||||
> Phase 3). `waggle-dance-bridge.ts` only *categorizes* an incoming `skill_share` subtype into
|
||||
> the `'handoff'` UI bucket (waggle-dance-bridge.ts:53). Diffusion is **out of scope for the
|
||||
> install-sync store** — it is a separate signal stream (relevant to screen 12, not 09).
|
||||
|
||||
---
|
||||
|
||||
## 3. The FE today — `MarketplaceApp` federates at READ, holds no shared state
|
||||
|
||||
`apps/web/src/components/os/apps/MarketplaceApp.tsx` (routed at `/marketplace` via
|
||||
`MarketplaceRoute.tsx`; mounted bare in Desktop) is the existing Phase-4B six-facet Extend
|
||||
surface. How it works today:
|
||||
- `loadFacet(f, q)` (MarketplaceApp.tsx:116) fires **N parallel adapter calls** (one per facet)
|
||||
and merges results into a **local** `useState<Extension[]>` (line 105) via the pure
|
||||
normalizers in `lib/extension-catalog.ts`.
|
||||
- Install dispatch (`handleInstall`, MarketplaceApp.tsx:224) **only works for
|
||||
`kind:'package'`** (marketplace registry rows). Connectors, MCP catalog rows, agents,
|
||||
models, templates are `kind:'federated'` → `installable:false` → render an **"Open in
|
||||
<app>"** deep-link, NOT an install button (`ExtensionCard.tsx:72`). Skill *packs* are
|
||||
`installable:false` too (no install-pack route).
|
||||
- On a successful install it mutates ONLY its own local array
|
||||
(`setExtensions(prev => prev.map(...installed:true))`, line 231). **Nothing else on the
|
||||
screen or app knows.** Unmount = state gone, re-fetch on next mount.
|
||||
|
||||
### Card today vs. design
|
||||
`ExtensionCard.tsx` shows a generic **`Install` / `Remove`** button + a static
|
||||
`Installed`/`Available` `StatusBadge`. It has **none** of:
|
||||
- type-aware verbs (`Add`/`Connect`/`Enable`) — design `VERB` map, marketplace.html:216
|
||||
- a per-item `installing` micro-state (`Adding…`/`Signing in…`/`Enabling…`)
|
||||
- a `+`/`✓` affordance keyed off a shared installed Set
|
||||
|
||||
### Variation B today — `CapabilityRequestCard` (a partial, divergent precursor)
|
||||
`apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx` already surfaces an
|
||||
**inline install affordance in chat**, parsed out of agent text by
|
||||
`capability-request-parser.ts` (a `<!--waggle:capability_request {...}-->` marker or a legacy
|
||||
`install_capability with name "X"` phrasing). BUT:
|
||||
- It handles **only `skill` (starter-pack) and `marketplace`** kinds — **no connector vault
|
||||
approval, no MCP enable** (the design's headline Variation-B example is a *Salesforce
|
||||
connector* with the "token → vault" approval, which this card cannot render).
|
||||
- It owns its **own local `phase` state** (`'pending'|'installing'|'installed'|...`, line 17/32)
|
||||
— completely disconnected from the grid. Installing here never updates the grid's count bar
|
||||
or card; installing in the grid never flips this card to "installed."
|
||||
- It is **not vault-aware** and has no "token goes to your vault" approve-row.
|
||||
|
||||
So: two install UIs, two private states, three backends, zero shared store, no count bar.
|
||||
|
||||
---
|
||||
|
||||
## 4. What "one store + cross-view reactivity" requires
|
||||
|
||||
### 4a. The design's reference model (what we must reproduce, for real)
|
||||
`marketplace.html` is the spec in code: a single module-level `installed: Set` + `installing:
|
||||
Set`, a `btnHTML(item)` that reads those Sets for `[idle, in-progress, done]` per `item.k`
|
||||
(`VERB` map), ONE delegated click handler (`document.addEventListener('click', … install(id))`),
|
||||
and a `renderAll()` that re-paints **grid + picks + count-bar + inline-chat card** off the same
|
||||
two Sets. `install(id)` flips `installing→installed` with a type-keyed delay, fires a toast, and
|
||||
re-renders everything. The inline-chat `renderInline()` (line 299) reads the SAME `installed`
|
||||
Set — that is the entire "sync" mechanism.
|
||||
|
||||
### 4b. FE store shape (the new artifact PR4 introduces)
|
||||
A React context/store — call it `InstallStore` (the BUILD-PLAN's "new install store") — holding:
|
||||
- `installed: Map<extKey, InstalledRecord>` and `installing: Set<extKey>` where `extKey` is the
|
||||
existing namespaced id (`extension-catalog`'s `connector:slack`, `mcp:fs`, `pkg:42`,
|
||||
`skill:teardown`) so it federates across types without collision.
|
||||
- `install(ext)` — **type-aware dispatcher** routing to the right adapter call:
|
||||
- `skill` → `adapter.installSkill(id, source, packageId?)` / `installPack` (instant)
|
||||
- `connector` → vault approval → `addVaultSecret` + `connectConnector(id)` (~1.1s, gated)
|
||||
- `mcp` → `adapter.installMcp(mcpId)` (PRO-gated; SecurityGate)
|
||||
- `package` (registry) → `adapter.installMarketplacePackage(packageId)`
|
||||
optimistically add to `installing`, on success move to `installed` + toast, on failure roll back.
|
||||
- A subscription so the grid, the agent-pick box, the count bar, AND the chat
|
||||
`CapabilityRequestCard` all read the same Sets and re-render on change (the cross-view
|
||||
reactivity = React context consumers, replacing today's per-component `useState`).
|
||||
- Pattern to match: this codebase already uses small custom contexts (`ServiceProvider`,
|
||||
`ShellContext`, `ThemeProvider` in `apps/web/src/providers/`) and per-domain hooks
|
||||
(`useChat`, `useMemory`, `useWorkspaces` in `apps/web/src/hooks/`). The install store should
|
||||
be a sibling provider + a `useInstallStore()` hook — **no Redux/Zustand precedent in this repo.**
|
||||
|
||||
### 4c. Server aggregate (the gap)
|
||||
The store needs an **initial installed-set hydrate** and a **count**. Today that means fanning
|
||||
out the same N calls `MarketplaceApp.loadFacet` already does (`/api/skills`,
|
||||
`/api/connectors`, `/api/mcps`, `/api/marketplace/installed`) and merging. Options for the
|
||||
"N in this workspace" bar:
|
||||
- **Cheap path (no new route):** derive the count/chips on the FE from the existing per-type
|
||||
list endpoints the store already calls (each returns an `installed` flag/`status`). This is
|
||||
the surgical, ship-now option.
|
||||
- **Aggregate route (nicer):** a new `GET /api/extend/installed` (or `/api/marketplace/sync`)
|
||||
returning `{ items:[{key,type,name,installed}], count }` by merging the three backends server-
|
||||
side. The Marketplace already owns an "Extend" namespace (`extend.ts`, the audit feed); this
|
||||
would slot beside it. Mind the §7.5 mind-isolation rule: keep it workspace-scoped, no cross-
|
||||
mind reads.
|
||||
|
||||
### 4d. The three micro-state flows differ structurally — the store must encode that
|
||||
| type | adapter call | gate / approval | latency profile | persistence |
|
||||
|---|---|---|---|---|
|
||||
| skill | `installSkill`/`installPack` | none (bundled) | instant (~480ms) | FS file + `agentState` reload |
|
||||
| connector | `addVaultSecret`+`connectConnector` | **vault approval** ("token → vault"); may need OAuth | ~1.1s (sign-in) | vault credential |
|
||||
| mcp | `installMcp` | **PRO tier + SecurityGate** scan | ~720ms (spawn/enable) | `.mcp.json` + runtime |
|
||||
| package (registry skill/mcp) | `installMarketplacePackage` | **PRO tier + SecurityGate** | varies | marketplace.db install row |
|
||||
|
||||
The store's `install()` cannot be one uniform call — it is a switch on `ext.type`/`ext.kind`,
|
||||
each branch with its own optimistic/confirm/rollback semantics. Connector is the only branch
|
||||
that surfaces an interstitial approval before the optimistic flip.
|
||||
|
||||
---
|
||||
|
||||
## 5. Concrete deltas vs. the screen-09 contract
|
||||
|
||||
1. **No shared install store at all** — grid, agent-picks, and inline-chat each hold private
|
||||
state (or none). The store + cross-view reactivity is net-new (the BUILD-PLAN's "new install
|
||||
store").
|
||||
2. **No count bar** — the design's "N in this workspace" chip bar (`ibCount`/`ibChips`,
|
||||
marketplace.html:151-155) has no FE or server surface today.
|
||||
3. **Cards lack type-aware verbs + micro-states** — `ExtensionCard` shows generic
|
||||
`Install`/`Remove`; design wants `Add/Connect/Enable` × `idle/in-progress/done`.
|
||||
4. **Connectors & MCP are not installable from the grid today** — they render as federated
|
||||
"Open in <app>" deep-links (`installable:false`). The design wants a one-click
|
||||
Connect/Enable IN the grid that hits the real vault/runtime paths.
|
||||
5. **Variation B is partial** — `CapabilityRequestCard` exists but handles only skill+marketplace,
|
||||
owns disconnected local state, and has **no vault-aware connector approval** (the literal
|
||||
headline example of Variation B).
|
||||
6. **No centered agent-search "Ask the agent" + suggestion box** — MarketplaceApp has a plain
|
||||
filter search, not the "Describe what you want to do…" agent-pick surface with
|
||||
connector+skill+tool recommendations and "why" reasons. (This is the Variation-A search half;
|
||||
Slice-3's job is the *install state* under it, but the picks must read the same store.)
|
||||
7. **`installable:false` on skill packs** — no install-pack route server-side; either the store
|
||||
skips packs or PR4 adds the route.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration points a PR4 build would touch
|
||||
|
||||
**FE (new + edit):**
|
||||
- NEW: `apps/web/src/providers/InstallProvider.tsx` (or `hooks/useInstallStore.ts`) — the shared
|
||||
Set-based store + type-aware `install()` dispatcher + count selector. Mount alongside
|
||||
`ServiceProvider`/`ShellContext`.
|
||||
- EDIT: `apps/web/src/components/os/apps/MarketplaceApp.tsx` — read installed/installing from the
|
||||
store instead of local `useState`; add the count bar + agent-search/picks; route card installs
|
||||
through the store.
|
||||
- EDIT: `apps/web/src/components/os/apps/extend/ExtensionCard.tsx` — type-aware verbs +
|
||||
`idle/in-progress/done` micro-states off the store.
|
||||
- EDIT: `apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx` +
|
||||
`capability-request-parser.ts` — extend the `CapabilityRequest.kind` union to
|
||||
`'connector'|'mcp'` (with the vault approval row for connector), read/write the SHARED store.
|
||||
- REUSE: `apps/web/src/lib/extension-catalog.ts` (`Extension`, namespaced ids, `fromConnector`/
|
||||
`fromMcpCatalogRow`/`fromMarketplacePackage`/`fromSkillPack`) — the existing federation
|
||||
normalizers are the store's value type; today they hard-code `installable:false` for
|
||||
connectors/MCP — those flip true once the store can install them.
|
||||
|
||||
**FE adapter (already present — wire, don't recreate):** `adapter.installSkill` /
|
||||
`installPack` / `connectConnector` / `addVaultSecret` / `installMcp` /
|
||||
`installMarketplacePackage`; list/hydrate via `getSkills`/`getConnectors`/`getMcps`/
|
||||
`getMarketplace`/`getMarketplaceInstalled` equivalents.
|
||||
|
||||
**Server (mostly reuse; one optional new route):**
|
||||
- REUSE: `POST /api/skills/:id/install` (skills-aliases.ts), `POST /api/connectors/:id/connect`
|
||||
(connectors.ts:96), `POST /api/mcps/install` (mcps.ts:189), `POST /api/marketplace/install`
|
||||
(marketplace.ts:181) — the install verbs already exist and audit.
|
||||
- REUSE for hydrate: `GET /api/skills`, `GET /api/connectors`, `GET /api/mcps`,
|
||||
`GET /api/marketplace/installed`.
|
||||
- OPTIONAL NEW: `GET /api/extend/installed` (aggregate installed-set + count) beside
|
||||
`extend.ts` — only if the FE doesn't derive the count from the per-type lists. Keep
|
||||
workspace-scoped (CLAUDE.md §7.5 mind-isolation).
|
||||
- DO NOT TOUCH: the install-audit substrate as a source of truth (`install_audit` is OSS-excluded
|
||||
+ append-only).
|
||||
|
||||
**Shared types:** `packages/shared/src/types.ts` `EXTENSION_TYPES`/`ExtensionType` (line 375) is
|
||||
the facet vocabulary the store keys on — no change needed unless a `package` vs domain
|
||||
discriminator is wanted.
|
||||
|
||||
---
|
||||
|
||||
## 7. Risks
|
||||
|
||||
- **Three backends, three "installed" definitions** — FS file vs vault credential vs
|
||||
`.mcp.json`⋈runtime. A naive single boolean store will drift from reality (e.g. an MCP that
|
||||
failed to `start()` is persisted-but-not-running). The store's record must carry enough state
|
||||
(`status`/`state`) to stay honest, mirroring `getMcps`'s `status` field.
|
||||
- **Tier + SecurityGate gating is real** — MCP and marketplace installs are PRO-gated and can be
|
||||
scan-blocked (403/422 with `{requiresApproval, blocked, severity}`). The store's optimistic
|
||||
flip must roll back on these and route to the UpgradeModal/ApprovalModal, exactly as
|
||||
`MarketplaceApp.handleInstall` (line 238) and `MCPHubApp` already do. Don't let the count bar
|
||||
show an item that the gate rejected.
|
||||
- **Connector OAuth complexity** — the "Connect → Signing in…" flow may be a real OAuth redirect
|
||||
(oauth.ts) for some connectors, not just a token paste. The ~1.1s mock latency hides a
|
||||
potentially multi-step, navigation-away flow; the store needs a pending/await state that
|
||||
survives that.
|
||||
- **Reflecting external installs** — installs done in the dedicated apps (`ConnectorsApp`,
|
||||
`MCPHubApp`, skills center) must also update the shared store, or the count bar lies. Either
|
||||
those apps adopt the store too, or the store re-hydrates on focus/navigation.
|
||||
- **Audit-vocabulary mismatch** — `install_audit`'s action enum has no `'enabled'`/`'connected'`/
|
||||
`'synced'` verbs (connectors map sync→`'approved'`, connectors.ts:225). If the store ever reads
|
||||
the audit feed for state, it inherits this lossiness. Read from the per-type list endpoints
|
||||
instead.
|
||||
- **`apps/web` is the only typechecked surface** (CLAUDE.md §2) — a new provider with subtle
|
||||
types is fine, but any server aggregate route runs under `tsx` transpile-only and won't be
|
||||
typechecked by `npm run build`; run `tsc -p packages/server` explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open questions for the founder/lead
|
||||
|
||||
1. **Aggregate route vs FE-derived count** — ship the count bar by deriving from the existing
|
||||
per-type list endpoints (zero new server surface, fastest), or add a real
|
||||
`GET /api/extend/installed`? (Recommend FE-derive for PR4; promote to a route only if reused.)
|
||||
2. **Do the dedicated apps adopt the store too?** The "installing in ANY view reflects in ALL"
|
||||
contract technically includes `ConnectorsApp`/`MCPHubApp`/skills center, not just the three
|
||||
Marketplace surfaces. In-scope for PR4, or PR4 covers grid+picks+inline only and the store
|
||||
re-hydrates on nav?
|
||||
3. **Connector install in-grid = real OAuth?** Some connectors are token-paste (vault), some are
|
||||
OAuth redirect. Does the grid's one-click "Connect" do the full sign-in inline, or open the
|
||||
Connector Hub for OAuth ones while doing token-paste inline? (Affects the micro-state UX.)
|
||||
4. **Skill packs installable?** They are browse-only today (no install-pack route). Add a real
|
||||
`POST /api/marketplace/install-pack` for PR4, or keep packs browse-only and have the store
|
||||
skip them?
|
||||
5. **Agent-pick search backing** — is the "Ask the agent" suggestion box a real agent call
|
||||
(`/api/command` / the agent loop) returning connector+skill+tool picks, or a heuristic FE
|
||||
matcher like the mock's `recs` map? (Slice 3 only owns that the picks read the shared store;
|
||||
the recommendation engine itself may be another slice.)
|
||||
6. **PRO-gating in the count** — should PRO-gated items (MCP, marketplace) appear installable to
|
||||
FREE users with an upsell on click, or render gated up front? (`MarketplaceApp` currently
|
||||
lets the click 403 → UpgradeModal.)
|
||||
|
||||
---
|
||||
|
||||
## 9. Key files (quick index)
|
||||
|
||||
| path | role |
|
||||
|---|---|
|
||||
| `docs/design_handoff_waggle_app/SCREENS.md` §09 (189-207) | the screen-09 contract |
|
||||
| `docs/design_handoff_waggle_app/design-files/screens/marketplace.html` (203-310) | canonical "sync" reference impl (installed/installing Sets + renderAll) |
|
||||
| `apps/web/src/components/os/apps/MarketplaceApp.tsx` | existing Extend grid — federates at read, local state only |
|
||||
| `apps/web/src/lib/extension-catalog.ts` | pure facet normalizers + namespaced ids = the store's value type |
|
||||
| `apps/web/src/components/os/apps/extend/ExtensionCard.tsx` | grid card — generic Install/Remove, needs type-aware verbs |
|
||||
| `apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx` | Variation-B precursor — skill+marketplace only, private state, no vault approval |
|
||||
| `apps/web/src/components/os/apps/chat-blocks/capability-request-parser.ts` | inline-card parser (marker + legacy phrasing) |
|
||||
| `apps/web/src/lib/adapter.ts` (1240/1338/1958/2004 …) | all install/list verbs already exist |
|
||||
| `packages/server/src/local/routes/skills.ts` | skills install (FS files + agentState reload + audit) |
|
||||
| `packages/server/src/local/routes/connectors.ts` | connectors install (vault credentials + audit) |
|
||||
| `packages/server/src/local/routes/mcps.ts` | MCP install (.mcp.json + runtime, PRO+SecurityGate) |
|
||||
| `packages/server/src/local/routes/marketplace.ts` | registry install/uninstall/installed + SecurityGate |
|
||||
| `packages/server/src/local/routes/extend.ts` + `extend/InstallAuditPanel.tsx` | shared install-audit feed (the ONLY cross-type surface today) |
|
||||
| `packages/server/src/local/waggle-dance-bridge.ts` (53) | only categorizes incoming skill_share → not install-sync |
|
||||
| `packages/shared/src/types.ts` (375) | `EXTENSION_TYPES` facet vocabulary |
|
||||
| `apps/web/src/providers/{ServiceProvider,ShellContext,ThemeProvider}.tsx` | the context pattern the new InstallProvider should follow |
|
||||
348
docs/redesign-warm-hive/pr4-recon/04-inline-in-chat.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# PR4 Recon — Slice 4: Inline-in-Chat Install (Marketplace Variation B)
|
||||
|
||||
> **Scope:** Screen 09 Marketplace, **Variation B** — Waggle offers a *missing connector*
|
||||
> mid-conversation with a vault-aware approval ("token goes to your vault"), then shows the
|
||||
> connected follow-up. Map the chat step/block rendering, the approval/confirmation infra,
|
||||
> and the vault-write path; identify the exact hook points a PR4 build would touch.
|
||||
>
|
||||
> **READ-ONLY recon.** No source modified. Citations are `file:line` against
|
||||
> `feature/warm-hive-pr4` (working tree at recon time).
|
||||
|
||||
---
|
||||
|
||||
## 1. The design contract (what Variation B must do)
|
||||
|
||||
From `docs/design_handoff_waggle_app/SCREENS.md:199-207`:
|
||||
|
||||
- **Variation B (Inline in chat):** "the same picker surfaced mid-conversation — Waggle
|
||||
offers a missing Salesforce connector with a vault-aware approval ('token goes to your
|
||||
vault'), then shows the connected follow-up."
|
||||
- **CRITICAL — shared install state ("sync"):** "one store powers the grid, the agent picks,
|
||||
AND the inline card." Type-aware one-click flows with progress→done micro-states:
|
||||
- **skill** Add→Adding…→Added (instant)
|
||||
- **connector** Connect→Signing in…→Connected (~1.1s, **token→vault**)
|
||||
- **MCP** Enable→Enabling…→Enabled
|
||||
- Each fires a toast + updates the count bar. **Installing in any view reflects in all.**
|
||||
|
||||
PR4 BUILD-PLAN row (`docs/redesign-warm-hive/BUILD-PLAN.md:142`):
|
||||
`PR4 | Marketplace + shared install store ("sync") (grid + agent-pick + inline-in-chat) | MarketplaceApp, new install store | 09`
|
||||
|
||||
So Variation B is **one of three consumers** of a single shared install store. This slice maps
|
||||
the *chat-side* surface (how the agent raises the offer mid-turn, how the FE renders the inline
|
||||
card, and how approval → connect → vault wires). The grid + store itself is the broader PR4 build.
|
||||
|
||||
---
|
||||
|
||||
## 2. End-to-end map of the chat turn (what exists today)
|
||||
|
||||
### 2.1 SSE event protocol (server → FE)
|
||||
|
||||
`POST /api/chat` (`packages/server/src/local/routes/chat.ts:351`) is the SSE endpoint. It hijacks
|
||||
the reply (`chat.ts:462`) and writes events with `sendEvent(event, data)` (`chat.ts:475-477`):
|
||||
|
||||
```
|
||||
event: <name>\ndata: <JSON>\n\n
|
||||
```
|
||||
|
||||
Event names emitted today (the wire vocabulary the FE switches on):
|
||||
`step`, `tool` / `tool_result` (auto_recall only), `token`, `gepa_choices`, `model_switch`,
|
||||
`approval_required`, `done`, `error`. (Note: the agent loop itself emits `tool_start`/`tool_end`
|
||||
for real tool calls — see the FE switch in §2.3 — via the agent-loop callbacks, not direct
|
||||
`sendEvent` in chat.ts.)
|
||||
|
||||
The **approval handshake** is the load-bearing primitive for Variation B. It lives in a
|
||||
per-request `pre:tool` hook registered at `chat.ts:909-1045`:
|
||||
|
||||
1. A tool reaches the gate; `needsConfirmationWithAutonomy(toolName, args, autonomyLevel)`
|
||||
decides if it gates (`chat.ts:916`).
|
||||
2. The hook enriches the request with **risk metadata** — `install_capability` gets a
|
||||
content-based `assessTrust()` (`chat.ts:960-982`), every other gated tool gets
|
||||
`classifyGatedToolRisk()` (`chat.ts:989-999`). `description` comes from
|
||||
`describeToolUse(toolName, input)` (`chat-helpers.ts:106`).
|
||||
3. It emits `sendEvent('approval_required', { requestId, toolName, input, sourceWorkspaceId,
|
||||
...trustMeta })` (`chat.ts:1005-1009`).
|
||||
4. It **blocks** on `new Promise<boolean>` registered in `server.agentState.pendingApprovals`
|
||||
keyed by `requestId` (`chat.ts:1020-1036`), with a 5-min auto-deny timeout.
|
||||
5. The FE posts the decision to `POST /api/approval/:requestId`
|
||||
(`packages/server/src/local/routes/approval.ts:10`), which calls `pending.resolve(approved)`
|
||||
(`approval.ts:31`) → the hook returns `{ cancel: true }` on deny (`chat.ts:1041`) or lets the
|
||||
tool run on approve.
|
||||
|
||||
This approval handshake is **exactly the mechanism Variation B needs** — a server-side pause
|
||||
mid-turn that surfaces an FE card and resumes on the user's click.
|
||||
|
||||
### 2.2 Block model (the FE message content type)
|
||||
|
||||
`apps/web/src/lib/types.ts:484-532` — `ContentBlock` is a discriminated union of:
|
||||
`TextContentBlock` | `StepContentBlock` | `ToolUseContentBlock` | `ModelSwitchContentBlock` |
|
||||
`ErrorContentBlock`. There is **no approval/install block type** — approvals are kept *out* of
|
||||
the block stream (see §2.4) and rendered as a separate singleton.
|
||||
|
||||
`StepContentBlock` (`types.ts:497-508`) already carries an **optional `provenance: { sources }`**
|
||||
field (PR3.5) — the precedent for type-specific metadata riding on a step block.
|
||||
|
||||
### 2.3 SSE → block reduction (`useChat`)
|
||||
|
||||
`apps/web/src/hooks/useChat.ts:125-273` consumes the SSE stream and reduces events into
|
||||
`blocks[]` on the last assistant message. The relevant cases:
|
||||
|
||||
- `step` → pushes a `StepContentBlock` (`useChat.ts:152-175`), copying `data.provenance.sources`.
|
||||
- `tool_start` / `tool_end` → push/patch a `ToolUseContentBlock` (`useChat.ts:177-222`).
|
||||
- `approval_request` / `approval_required` → `setPendingApproval(data as ApprovalRequest)` and
|
||||
**`return msgs`** — i.e. deliberately does NOT add a block (`useChat.ts:258-263`).
|
||||
- `approveAction(requestId, approved, { always })` → `adapter.respondApproval(...)` then
|
||||
`setPendingApproval(null)` (`useChat.ts:317-333`).
|
||||
|
||||
So today there is **one** pending approval at a time, held in component state, not in the block
|
||||
stream. The hook returns `{ messages, isLoading, sendMessage, clearHistory, pendingApproval,
|
||||
approveAction }` (`useChat.ts:335`).
|
||||
|
||||
### 2.4 Block rendering
|
||||
|
||||
`apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx:53-104` walks `blocks[]`:
|
||||
- consecutive `step` blocks are grouped into one collapsible **`ActivityStream`** card
|
||||
(`BlockRenderer.tsx:30-51`) — the "magic" surface.
|
||||
- `tool_use` blocks route to **`ArtifactBlock`** (completed file-writes — `ArtifactBlock.tsx:21-28`
|
||||
is the routing predicate `isArtifactBlock`) or the generic `ToolUseBlock`.
|
||||
- `text` / `model_switch` / `error` each have a renderer.
|
||||
|
||||
`ArtifactBlock.tsx` is the **closest existing precedent** for what an inline install card should be:
|
||||
a tool-result that, when completed, renders as a rich actionable card (icon + name + button) instead
|
||||
of a debug row. An inline-install card is the same pattern for a *connector/skill/MCP* tool result.
|
||||
|
||||
### 2.5 Approval rendering (the inline card that exists)
|
||||
|
||||
`ChatApp` renders the singleton approval **inside the message list** at the bottom:
|
||||
`{pendingApproval && <ApprovalGate request={pendingApproval} onRespond={onApprove} />}`
|
||||
(`ChatApp.tsx:1213-1215`).
|
||||
|
||||
**`ApprovalGate`** (`ChatApp.tsx:225-311`) is already an in-thread, warm-token card:
|
||||
`--honey-wash` fill + attention border + `AlertTriangle` + a `RiskBadge` + `description` + a
|
||||
mono `toolName › inputSummary` line + `trustSource` + Approve / Always-allow / Not-now buttons +
|
||||
a "Show details" JSON toggle. It is **generic** (driven by `ApprovalRequest`), not connector-aware.
|
||||
|
||||
There is also a **reusable warm primitive** `InlineApprovalCard`
|
||||
(`apps/web/src/components/os/warm/InlineApprovalCard.tsx`, exported from `warm/index.ts:19`) — a
|
||||
cleaner card that takes an `ApprovalRequest`, `title`, `onApprove`/`onDecline`/`onAlwaysAllow`,
|
||||
and an `approveLabel`. Its **default title is literally "Approve before I leave your machine"** —
|
||||
already in the vault-aware register Variation B wants. NOTE: `ApprovalGate` (the one actually
|
||||
wired in ChatApp) and `InlineApprovalCard` are **two parallel implementations of the same idea**;
|
||||
both share the risk vocabulary via `lib/risk-display.ts`. A build should pick one.
|
||||
|
||||
`ApprovalRequest` shape (`apps/web/src/lib/types.ts:410-430`): `{ requestId, toolName, description,
|
||||
input, rawJson?, sourceWorkspaceId?, riskLevel?, approvalClass?, trustSource?, assessmentMode?,
|
||||
explanation?, permissions? }`. All of these are already on the wire from `chat.ts:1005-1009`.
|
||||
|
||||
---
|
||||
|
||||
## 3. The install / connect / vault paths (what an "approve" must trigger)
|
||||
|
||||
There are **three different install backends today**, none of which is currently reachable as a
|
||||
single agent-loop "offer a missing connector" tool:
|
||||
|
||||
### 3.1 Skill install — agent-loop reachable (the only one that is)
|
||||
|
||||
`packages/agent/src/skill-tools.ts:480-668` — the `install_capability` tool. It copies a curated
|
||||
starter-skill `.md` into the skills dir (`skill-tools.ts:631-633`), runs a `SecurityGate` scan,
|
||||
records install-audit rows, hot-reloads via `onSkillsChanged()`, and returns the skill content.
|
||||
**No vault.** This is the "skill: Add→Adding…→Added (instant)" lane. It already flows through the
|
||||
chat approval gate (it is in `ALWAYS_CONFIRM`, `confirmation.ts:19`) and gets the **richest** trust
|
||||
metadata on the approval event (`chat.ts:960-982`).
|
||||
|
||||
### 3.2 Connector connect — HTTP route, NOT agent-loop reachable
|
||||
|
||||
`POST /api/connectors/:id/connect` (`packages/server/src/local/routes/connectors.ts:96-156`):
|
||||
- validates the connector exists in `connectorRegistry`,
|
||||
- requires `token` or `apiKey` in the body,
|
||||
- **writes the token to vault**: `fastify.vault.setConnectorCredential(id, { type, value,
|
||||
refreshToken, expiresAt, scopes })` (`connectors.ts:122-128`) — **this is the "token→vault"
|
||||
step the design names**,
|
||||
- re-initializes the connector (`connector.connect(fastify.vault)`, `connectors.ts:138`),
|
||||
- records an install-audit row (`connectors.ts:145-153`).
|
||||
|
||||
FE adapter: `adapter.connectConnector(id, credentials)` → POSTs that route
|
||||
(`apps/web/src/lib/adapter.ts:1971-1979`).
|
||||
|
||||
**There is no agent tool that calls this.** The agent can only *discover* connectors via
|
||||
`find_connector` / `list_connector_categories` (`packages/agent/src/connector-search.ts:161-265`),
|
||||
which return catalog JSON (name, installCmd, url) as text — the agent literally cannot connect one.
|
||||
|
||||
### 3.3 Connector OAuth — separate browser-redirect flow
|
||||
|
||||
`packages/server/src/local/routes/oauth.ts` — `GET /api/oauth/:provider/authorize` →
|
||||
provider page → `GET /api/oauth/:provider/callback` stores tokens in vault under
|
||||
`${provider}_oauth_token` (keyed by **provider**, not connector id — see
|
||||
`connectors.ts:16-23` `OAUTH_PROVIDER_FOR_CONNECTOR`). Only 5 providers configured
|
||||
(github/slack/google/notion/jira, `oauth.ts:26-62`) and each needs app `client_id`/`client_secret`
|
||||
pre-seeded in vault. This is the heavyweight path; the design's "token goes to your vault" implies
|
||||
the **lightweight token-paste** path of §3.2, not OAuth.
|
||||
|
||||
### 3.4 Connector tools become live on the NEXT turn
|
||||
|
||||
Important for "the connected follow-up": connector action tools are **dynamic**.
|
||||
`connectorRegistry.generateTools()` generates `connector_<id>_<action>` tools **only for connected
|
||||
connectors** (`packages/server/src/local/index.ts:1005-1006` comment + `1034`). The agent loop
|
||||
rebuilds `effectiveTools` per request (`chat.ts:1048-1073` → `buildToolsForWorkspace`). So once a
|
||||
connector is connected mid-conversation, its tools appear on the **next** user turn (or next loop
|
||||
iteration if connect happens inside the same turn before tool-pool rebuild — but the rebuild is
|
||||
per-`/api/chat` call, so realistically next turn). `describeToolUse` already formats
|
||||
`connector_<id>_<action>` as "<action> via <id>" (`chat-helpers.ts:192-196`).
|
||||
|
||||
### 3.5 MCP enable — yet another backend
|
||||
|
||||
MCP servers install through the MCP Hub (`MCPHubApp.tsx`, security scan + scope + approval). Not
|
||||
chat-loop reachable today. Out of the *critical* path for Variation B's "Salesforce connector"
|
||||
example, but the shared store must cover the "MCP Enable→Enabling…→Enabled" lane.
|
||||
|
||||
---
|
||||
|
||||
## 4. The DELTA — what's missing vs the Screen-09 contract
|
||||
|
||||
| # | Contract requirement | Current state | Gap |
|
||||
|---|---|---|---|
|
||||
| D1 | Agent can **offer a missing connector mid-conversation** | Agent can only `find_connector` (returns catalog text); no tool connects one | **No `connect_capability`/`offer_connector` tool** that raises an approval whose approve-side writes a token to vault. Needs a new agent tool OR a server-side "offer" step. |
|
||||
| D2 | Inline card is **vault-aware** ("token goes to your vault") | `ApprovalGate`/`InlineApprovalCard` are generic; show `toolName › input` + risk | No connector-typed variant: no token-input field, no "encrypted in your local vault" copy, no Connect→Signing in…→Connected micro-states. |
|
||||
| D3 | **Token→vault** on approve (~1.1s) | `POST /api/connectors/:id/connect` writes vault, but is reached only from MarketplaceApp/ConnectorsApp FE | The chat approve path resolves a `boolean` promise (`approval.ts:31`); it has **no channel to carry a token** nor to invoke `connectConnector`. The approval contract is boolean-only. |
|
||||
| D4 | **Connected follow-up** shown in thread | Connector tools regenerate per request; agent can use them next turn | No explicit "connected" confirmation block; the follow-up is implicit. Needs a success block/toast + (optionally) auto-continue of the original ask. |
|
||||
| D5 | **Shared install store ("sync")** — one store, all 3 views reflect | `MarketplaceApp` uses **local `useState`** (`installing`/`extensions`, `MarketplaceApp.tsx:108`, `:231`); `ConnectorsApp`/`MCPHubApp` each own their own state | **No shared store.** Installing in chat would not reflect in the grid or count bar. This is the central PR4 artifact ("new install store"). |
|
||||
| D6 | Type-aware **micro-states** + toast + count-bar update | Skill install returns text; connector connect returns `{connected:true}`; toasts exist per-app (`useToast`) | No unified progress→done state machine keyed by kind; no count bar; no cross-view toast. |
|
||||
| D7 | Approval can carry **kind** (skill/connector/MCP) | `approval_required` carries `toolName` + trust meta, but kind is inferred from toolName | A connector offer needs an explicit `kind: 'connector'` + connector `id`/`name`/`why` so the card renders type-aware. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Exact integration points a PR4 build would touch
|
||||
|
||||
### Server (agent loop + routes)
|
||||
- **New agent tool** (e.g. `offer_connector` / `connect_capability`) in a new file under
|
||||
`packages/agent/src/` (sibling to `connector-search.ts`), registered into `baseTools`
|
||||
(`packages/server/src/local/index.ts:759`). It should be **gated** (add to `ALWAYS_CONFIRM`,
|
||||
`packages/agent/src/confirmation.ts:16-26`, or rely on `connector_` prefix patterns) so it
|
||||
hits the chat approval hook.
|
||||
- **`chat.ts:909-1045` pre:tool hook** — extend the trust-metadata branch (currently special-cases
|
||||
`install_capability` at `chat.ts:960`) to emit a **connector-typed** `approval_required` payload
|
||||
(`kind: 'connector'`, connector `id`/`name`, "why", and a flag that a token field is needed).
|
||||
- **`describeToolUse`** (`chat-helpers.ts:106-201`) — add a case for the new tool so the card's
|
||||
description line is specific ("Connect Salesforce — token stored in your local vault").
|
||||
- **Approval contract widening** — `POST /api/approval/:requestId` (`approval.ts:10`) +
|
||||
`pendingApprovals` resolve currently carry only `boolean`. To pass a pasted **token** from the
|
||||
inline card to the connect step, EITHER:
|
||||
(a) the card calls `adapter.connectConnector(id, { token })` directly (writing vault via
|
||||
`connectors.ts:96`) and *then* approves the tool with a boolean (token never transits the
|
||||
approval channel — cleanest, reuses existing vault route), OR
|
||||
(b) widen the approval body to carry the token and have the new tool's `execute` call
|
||||
`setConnectorCredential`. Option (a) is the lower-risk path and keeps the vault write on the
|
||||
audited `/connect` route.
|
||||
- **Vault write** stays `fastify.vault.setConnectorCredential(id, …)` (`connectors.ts:122`) — do
|
||||
not build a parallel secret store (CLAUDE.md §7.1).
|
||||
|
||||
### FE (chat + store)
|
||||
- **`useChat.ts`** — the `approval_required` case (`useChat.ts:258-263`) sets a singleton
|
||||
`pendingApproval`. For Variation B the connector offer can stay on this channel (it IS an
|
||||
approval), but the payload must carry `kind`/connector fields so the renderer can branch.
|
||||
Alternatively introduce an **install/offer block type** in `ContentBlock` (`types.ts:484`) +
|
||||
`BlockRenderer` (`BlockRenderer.tsx`) so the offer lives *in* the thread like `ArtifactBlock`,
|
||||
surviving history reload — recommended for the "connected follow-up" persistence.
|
||||
- **New `ConnectorOfferCard`** (or extend `InlineApprovalCard`, `warm/InlineApprovalCard.tsx`) —
|
||||
type-aware card with a token field, "encrypted in your local vault" copy, and
|
||||
Connect→Signing in…→Connected micro-states. Render it from `ChatApp` where `ApprovalGate` renders
|
||||
today (`ChatApp.tsx:1213-1215`), branching on `pendingApproval.kind`.
|
||||
- **Shared install store** (the PR4 centerpiece, `BUILD-PLAN.md:142` "new install store") — a
|
||||
React context/zustand store keyed by capability id with `{ kind, state: idle|installing|done }`,
|
||||
consumed by `MarketplaceApp` (replacing its local `useState` at `MarketplaceApp.tsx:108/231`),
|
||||
`ConnectorsApp`, `MCPHubApp`, AND the chat offer card. The card's approve handler calls
|
||||
`adapter.connectConnector` and updates the store → grid + count bar reflect instantly.
|
||||
- **Adapter** — reuse `adapter.connectConnector(id, { token })` (`adapter.ts:1971`),
|
||||
`adapter.respondApproval(requestId, true)` (`adapter.ts:1757`), `adapter.installMarketplacePackage`
|
||||
(skills/packages), and the connector list `adapter.getConnectors()` (for the count bar).
|
||||
|
||||
### Recommended seam (lowest-risk wiring)
|
||||
1. New gated agent tool `offer_connector(id, why)` → emits connector-typed `approval_required`
|
||||
(no token in the tool args; the token is collected by the FE card).
|
||||
2. FE renders `ConnectorOfferCard`; on Connect it (a) `adapter.connectConnector(id, { token })`
|
||||
→ vault write on the audited route, (b) updates the shared install store (grid/count-bar sync),
|
||||
(c) `adapter.respondApproval(requestId, true)` to release the agent.
|
||||
3. The tool's `execute` returns "Connected — Salesforce tools now available"; the agent uses
|
||||
`connector_<id>_<action>` tools on the **next** turn (already live after `generateTools()`).
|
||||
4. A success block/toast renders the "connected follow-up".
|
||||
|
||||
---
|
||||
|
||||
## 6. Risks / sharp edges
|
||||
|
||||
- **Boolean-only approval channel.** The existing approval handshake resolves a `boolean`
|
||||
(`approval.ts:31`). Threading a secret token through it would put a credential on the approval
|
||||
wire — prefer the FE-calls-`/connect`-directly seam (Option 5a) so the token stays on the
|
||||
dedicated vault route.
|
||||
- **Two parallel inline-approval components** (`ApprovalGate` in `ChatApp.tsx:225` vs warm
|
||||
`InlineApprovalCard`). Only `ApprovalGate` is wired. Building a third card risks a 3-way drift;
|
||||
consolidate onto the warm primitive.
|
||||
- **Approval is a singleton, out-of-band of blocks** (`useChat.ts:262` returns without pushing a
|
||||
block). It does **not** survive history reload and there is only one at a time. If the offer must
|
||||
persist in the transcript / show a permanent "connected" follow-up, it needs to become a real
|
||||
`ContentBlock` (new type), which touches the block union, `useChat`, `BlockRenderer`, and history
|
||||
serialization.
|
||||
- **Connector tools are next-turn, not same-turn.** The "connected follow-up that actually uses the
|
||||
connector" won't have the `connector_<id>_*` tool in-loop on the same turn the connect happened
|
||||
(tool pool is built once per `/api/chat`, `chat.ts:1048`). A same-turn auto-continue would need an
|
||||
explicit re-dispatch.
|
||||
- **No shared store today** means a chat-side install silently diverges from the grid/count bar —
|
||||
the single most load-bearing PR4 requirement ("installing in any view reflects in all"). The store
|
||||
must land before any of the three surfaces is "done".
|
||||
- **MCP + skill lanes differ from connector.** Skill install is agent-reachable + vault-free;
|
||||
connector connect is vault-bound + HTTP-only; MCP is Hub-only. A unified "type-aware one-click"
|
||||
card must dispatch to three different backends behind one store interface.
|
||||
- **OAuth vs token-paste.** Some connectors (the Google family, jira, slack, github) are OAuth, not
|
||||
token-paste (`oauth.ts:26-62`). The vault-aware token field only fits `bearer`/`apiKey` connectors;
|
||||
OAuth connectors need the redirect flow, which cannot complete inside an inline card without a
|
||||
popup/redirect. The card must branch on `connector.authType`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open questions for the founder/lead
|
||||
|
||||
1. **Approval channel for tokens** — keep the approval handshake boolean and have the FE call
|
||||
`/connect` directly (audited vault route), or widen the approval body to carry the token?
|
||||
(Recommend the former.)
|
||||
2. **Offer as block vs singleton** — should the inline connector offer (and its "connected"
|
||||
follow-up) be a persistent `ContentBlock` in the transcript, or stay the ephemeral singleton
|
||||
approval? (Persistence implies a new block type + history serialization.)
|
||||
3. **One inline card or two** — consolidate `ApprovalGate` and `InlineApprovalCard` into the
|
||||
warm primitive before adding a connector variant?
|
||||
4. **OAuth connectors** — for OAuth-only connectors (Google/Slack/etc.), does Variation B fall
|
||||
back to "open the Connector Hub" or attempt an in-chat popup redirect? Token-paste only covers
|
||||
`bearer`/`apiKey` connectors.
|
||||
5. **Same-turn vs next-turn follow-up** — is "shows the connected follow-up" satisfied by a
|
||||
success toast + the tool being available next turn, or must the agent auto-continue and use the
|
||||
connector in the same turn (requires re-dispatch)?
|
||||
6. **Shared store shape** — new dedicated store, or extend an existing provider? It must be the
|
||||
single source the grid count bar, the agent-pick suggestion box, and this card all read/write.
|
||||
7. **What raises the offer** — a new gated agent tool the model calls when it hits a capability
|
||||
gap, or a server-side heuristic (e.g. tool-not-found → CapabilityRouter, `chat.ts:1080`) that
|
||||
injects the offer? The `CapabilityRouter` already exists for tool-not-found handling and could
|
||||
be the trigger.
|
||||
|
||||
---
|
||||
|
||||
## 8. Key files (quick index)
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `docs/design_handoff_waggle_app/SCREENS.md:189-207` | Screen-09 contract (Variation A/B + sync) |
|
||||
| `packages/server/src/local/routes/chat.ts:909-1045` | pre:tool approval hook — emits `approval_required`, blocks on `pendingApprovals` |
|
||||
| `packages/server/src/local/routes/chat.ts:475-477,1005-1009` | `sendEvent` + the approval payload shape |
|
||||
| `packages/server/src/local/routes/approval.ts:10-35` | `POST /api/approval/:requestId` → resolves the boolean promise |
|
||||
| `packages/server/src/local/routes/connectors.ts:96-156` | `POST /connect` — **token→vault** (`setConnectorCredential`) |
|
||||
| `packages/server/src/local/routes/oauth.ts` | OAuth redirect flow (heavyweight, 5 providers) |
|
||||
| `packages/agent/src/connector-search.ts:161-265` | `find_connector` / `list_connector_categories` (discovery only — no connect) |
|
||||
| `packages/agent/src/skill-tools.ts:480-668` | `install_capability` — the only agent-loop install (skills, vault-free) |
|
||||
| `packages/agent/src/confirmation.ts:16-26` | `ALWAYS_CONFIRM` gate set + `needsConfirmation` |
|
||||
| `packages/server/src/local/index.ts:759,1005-1034` | tool-pool assembly; connector tools generated for **connected** connectors only |
|
||||
| `packages/server/src/local/routes/chat-helpers.ts:106-201` | `describeToolUse` (approval description line) |
|
||||
| `apps/web/src/hooks/useChat.ts:258-333` | FE: `approval_required` → singleton `pendingApproval`; `approveAction` |
|
||||
| `apps/web/src/components/os/apps/ChatApp.tsx:225-311,1213-1215` | `ApprovalGate` inline card + its render slot |
|
||||
| `apps/web/src/components/os/warm/InlineApprovalCard.tsx` | reusable warm inline-approval primitive ("Approve before I leave your machine") |
|
||||
| `apps/web/src/components/os/apps/chat-blocks/ArtifactBlock.tsx` | precedent: tool-result → rich actionable in-thread card |
|
||||
| `apps/web/src/components/os/apps/chat-blocks/BlockRenderer.tsx` | block dispatch (where a new offer block would route) |
|
||||
| `apps/web/src/lib/types.ts:410-430,484-532` | `ApprovalRequest` + `ContentBlock` union |
|
||||
| `apps/web/src/lib/adapter.ts:1757,1971` | `respondApproval`, `connectConnector` |
|
||||
| `apps/web/src/components/os/apps/MarketplaceApp.tsx:108,221-231` | grid install — **local `useState`** (no shared store) |
|
||||
234
docs/redesign-warm-hive/pr4-recon/05-agent-pick-search.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# PR4 Recon — Slice 5: Agent-Search / Agent-Pick
|
||||
|
||||
**Screen 09 Marketplace · Variation A "agent-suggestion box"**
|
||||
"Describe what you want to do…" → agent recommends a **connector + skill + tool**, each with a **"why"** reason and an **install** button.
|
||||
|
||||
Branch: `feature/warm-hive-pr4` · READ-ONLY recon · cited file:line as of 2026-06-16.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The agent-pick engine **already exists and is good**: `searchCapabilities()` in
|
||||
`packages/agent/src/capability-acquisition.ts:179` takes a natural-language `need` and returns a
|
||||
ranked, deduped `AcquisitionProposal` of native tools / installed skills / starter-pack skills /
|
||||
marketplace packages — **each candidate already carries a `matchReason` ("why") and an
|
||||
`installAction`** plus a trust assessment. It fuses **keyword scoring** (no embeddings) across local
|
||||
sources with **pre-fetched marketplace FTS5 candidates**.
|
||||
|
||||
**The gap is entirely the surface, not the brain.** This engine is reachable **only as an agent tool**
|
||||
(`acquire_capability`, `packages/agent/src/skill-tools.ts:406`) invoked inside the chat loop — there is
|
||||
**no HTTP route** that the Screen-09 agent-search bar could `POST` a need to and render the
|
||||
three-up suggestion box. PR4 must add a thin REST endpoint over the existing `searchCapabilities()`
|
||||
(plus its deps assembly that today lives inline in `local/index.ts:620`) and a new
|
||||
`MarketplaceApp` agent-search UI. The matching scoring is single-recommendation today
|
||||
(`proposal.recommendation` is ONE candidate); the design wants **one-of-each-kind** (connector+skill+tool),
|
||||
which is a selection/grouping change on top of the existing ranked `candidates[]`, not new matching.
|
||||
|
||||
---
|
||||
|
||||
## What exists today (verified)
|
||||
|
||||
### 1. The matching brain — `searchCapabilities()` (the agent-pick core)
|
||||
`packages/agent/src/capability-acquisition.ts`
|
||||
|
||||
- **Input** (`SearchCapabilitiesInput`, :170): `{ need, installedSkills[], starterSkillsDir,
|
||||
nativeToolNames[], marketplaceCandidates[] }`. Marketplace candidates are **pre-fetched and passed
|
||||
in** — `searchCapabilities` itself does no IO except reading starter-skill `.md` files from disk
|
||||
(`loadStarterSkillsMeta`, :142).
|
||||
- **Algorithm** (no embeddings — pure keyword): `extractKeywords()` (:64, stop-word filtered) →
|
||||
`scoreMatch()` (:74, name-hit ×2 / content-hit ×1, normalized 0–1) across four source lanes:
|
||||
1. native tools (scored vs `NATIVE_TOOL_HINTS` map, :113);
|
||||
2. installed/active skills (:218);
|
||||
3. starter-pack skills not yet installed (:241);
|
||||
4. marketplace candidates, merging the FTS score when present via `Math.max(keywordScore, mkt.score)` (:270).
|
||||
- **Output** (`AcquisitionProposal`, :39): `{ need, gapDetected, summary, candidates[]≤8,
|
||||
recommendation, alreadyHandled }`. Each `CapabilityCandidate` (:27) already has the exact fields
|
||||
Screen-09 needs: `name`, `type` ('native'|'skill'|'plugin'|'mcp'|'connector'|'marketplace'),
|
||||
`availability`, `description`, `matchReason` (**the "why"**, built by `buildMatchReason` :99),
|
||||
`installAction` (string|null), and `trust`.
|
||||
- **Recommendation is SINGLE** (:307–314): picks the best installable (or best active if
|
||||
already-handled). It does **not** group into connector+skill+tool. The full ranked `candidates[]`
|
||||
is there to do that, but the grouping logic does not exist yet.
|
||||
- **`summary`** is a markdown string built for the **chat** surface (`buildProposalSummary`, :330): it
|
||||
even **emits the inline-install marker** (`<!--waggle:capability_request {...}-->`, :385) verbatim for
|
||||
the chat card. This is debug/chat-grade prose — a UI agent-suggestion box would consume the
|
||||
**structured `candidates`/`recommendation`, not `summary`**.
|
||||
|
||||
### 2. How it's invoked today — the agent tool `acquire_capability`
|
||||
`packages/agent/src/skill-tools.ts:404–478`
|
||||
|
||||
- Tool `acquire_capability` (param: `need`) gathers deps: `getInstalledSkills()`, `starterSkillsDir`,
|
||||
`nativeToolNames`, and calls `deps.searchMarketplace(need)` (graceful try/catch) to pre-fetch
|
||||
marketplace candidates, then calls `searchCapabilities(...)` and **returns `proposal.summary`** (the
|
||||
markdown string) to the model. Audit event recorded on gap (:461).
|
||||
- Companion tool `install_capability` (:482) installs **starter-pack skills only** (validated by
|
||||
`validateInstallCandidate`, capability-acquisition.ts:424 — rejects any source ≠ `starter-pack`).
|
||||
- The deps are wired in `packages/server/src/local/index.ts:620` (`createSkillTools({...})`):
|
||||
- `nativeToolNames` = union of mind/system/plan/git/document tool names (:624);
|
||||
- `getInstalledSkills` = live `server.agentState.skills` (:631, hot-reloadable);
|
||||
- `searchMarketplace` = `marketplaceDb.search({ query, limit: 10 })` mapped to `MarketplaceCandidate[]`
|
||||
(:641–656) — note `score` is hardcoded `undefined` (FTS rank not surfaced through the API).
|
||||
|
||||
### 3. The marketplace search it sits on
|
||||
`packages/server/src/local/routes/marketplace.ts:57` — `GET /api/marketplace/search`
|
||||
→ `MarketplaceDB.search()` (`packages/marketplace/src/db.ts:86`).
|
||||
|
||||
- FTS5 over the `packages` table. **Critically, `db.search` already tolerates a verbose NL `need`**:
|
||||
`toFtsMatchQuery()` (db.ts:91–99) relaxes the raw string into an OR-of-prefixes and falls back to an
|
||||
unfiltered listing rather than throwing — so the agent's natural-language need works as-is.
|
||||
- `SearchResult` (`types.ts:233`): `{ packages[], total, facets{types,categories,sources},
|
||||
installedCount }`. Each `MarketplacePackage` carries `waggle_install_type` ('skill'|'connector'|'mcp')
|
||||
→ **the kind badge**, `package_type`, `description`, `downloads` (install count), and (route-annotated
|
||||
at marketplace.ts:105) `installed`, `scanStatus`. **`installedCount`** is the natural source for the
|
||||
"N in this workspace" count bar (currently catalog-wide, not workspace-scoped — see Gaps).
|
||||
|
||||
### 4. The parallel, narrower "recommend" path (skills-only) — DO NOT confuse with agent-pick
|
||||
`packages/agent/src/skill-recommender.ts` (`SkillRecommender.recommend(context, topN)`, class at :118).
|
||||
- Multi-signal keyword + bigram + synonym-cluster matcher over **installed skills only** (no
|
||||
marketplace, no connectors, no MCP). Returns `SkillRecommendation[] = {skillName, reason,
|
||||
relevanceScore}` — also a "why" (`reason`), but skills-only.
|
||||
- Exposed over HTTP at `GET /api/skills/suggestions?context=&topN=`
|
||||
(`packages/server/src/local/routes/skills.ts:393–406`). This is the **only** existing HTTP surface
|
||||
that returns "what should I use" with a reason — but it's the wrong shelf (skills only, already
|
||||
installed) for Screen-09's connector+skill+tool suggestion box. Useful as a *prior-art pattern* for
|
||||
shaping the new route.
|
||||
|
||||
### 5. The router (a third matcher) — for completeness
|
||||
`packages/agent/src/capability-router.ts` (`CapabilityRouter.resolve(query)`, :58). Maps a query to
|
||||
ranked routes across native/connector/skill/plugin/mcp/subagent with confidences. **Resolution, not
|
||||
recommendation** — returns "where could this be handled" not "install this". Connector lane (:85)
|
||||
knows `connected` status and emits a suggestion when not connected. Not currently HTTP-exposed; a
|
||||
secondary input if PR4 wants live-connector awareness in the suggestion box.
|
||||
|
||||
### 6. The inline-card render path (the "sync" downstream, shared with the grid)
|
||||
- Parser: `apps/web/src/components/os/apps/chat-blocks/capability-request-parser.ts` — `segmentText()`
|
||||
splits agent text on the `<!--waggle:capability_request {name,source,reason}-->` marker (and a legacy
|
||||
phrasing) into install-card segments, deduped by `source::name`.
|
||||
- Card: `apps/web/src/components/os/apps/chat-blocks/CapabilityRequestCard.tsx` — renders the
|
||||
**pending→installing→installed/failed** micro-states (:17,:107–149) with a "why" line from
|
||||
`request.reason` (:103). Install routing already branches by source: marketplace →
|
||||
`adapter.searchMarketplace` then `adapter.installMarketplacePackage(pkg.id)` (:48–54, tier-gated, 403
|
||||
→ UpgradeModal); starter-pack → `adapter.installPack` (:69). **This is exactly the type-aware
|
||||
one-click flow §09 asks for, already built for the inline-chat variation (Variation B).**
|
||||
|
||||
### 7. Adapter methods already present (the install actions)
|
||||
`apps/web/src/lib/adapter.ts`: `searchMarketplace(query,limit)` (:1334), `installMarketplacePackage(id)`
|
||||
(:1338), `installPack(skillId)` (:1292), `connectConnector(id,creds)` (:1971), `installMcp(mcpId)`
|
||||
(:2017). All three install kinds in the §09 sync spec (skill Add / connector Connect / MCP Enable)
|
||||
have adapter coverage.
|
||||
|
||||
---
|
||||
|
||||
## Gaps vs the Screen-09 contract
|
||||
|
||||
1. **No HTTP route for agent-pick.** `searchCapabilities()` is reachable ONLY inside the chat agent
|
||||
loop via the `acquire_capability` tool. The Screen-09 centered agent-search bar ("Ask the agent")
|
||||
needs a `POST /api/marketplace/agent-search` (or similar) that runs `searchCapabilities` and returns
|
||||
**structured candidates** (not the chat `summary` markdown). Must be built on top of the existing
|
||||
engine.
|
||||
|
||||
2. **Returns chat-grade `summary`, not a structured suggestion box.** The tool returns
|
||||
`proposal.summary` (markdown for the model). The UI needs the raw `candidates[]`/`recommendation`
|
||||
JSON. The data is computed (proposal object) but **discarded** at skill-tools.ts:476 — a route would
|
||||
return the object directly.
|
||||
|
||||
3. **Single recommendation, not one-of-each-kind.** `proposal.recommendation` is ONE candidate
|
||||
(capability-acquisition.ts:307). §09 wants **connector + skill + tool** (three, one per kind) each
|
||||
with its own why + install. Needs a small grouping pass over the ranked `candidates[]` (top per
|
||||
`type`/`availability`) — new logic, but trivial given candidates already carry `type`.
|
||||
|
||||
4. **Marketplace deps assembly lives inline in `local/index.ts`, not reusable.** The
|
||||
`getInstalledSkills` / `nativeToolNames` / `searchMarketplace` closure that feeds the tool is
|
||||
constructed once at server boot (index.ts:620). A new route needs the same deps — either lift this
|
||||
into a shared helper or have the route reconstruct it. (Native tool names, in particular, are only
|
||||
assembled in that closure.)
|
||||
|
||||
5. **"N in this workspace" count bar is catalog-wide, not workspace-scoped.** `installedCount`
|
||||
(db.search → types.ts:242) counts all installed packages globally; the marketplace DB is not
|
||||
workspace-partitioned. §09's per-workspace chip count has **no backing field** today.
|
||||
|
||||
6. **No "example chips" / suggested-need seeding.** Pure UI; no backend. Can be static or derived
|
||||
from persona `suggestedCommands` — out of agent-pick scope.
|
||||
|
||||
7. **No shared install-state store on the frontend.** §09's CRITICAL "sync" (one store powers grid +
|
||||
agent-pick + inline card, installing in any view reflects in all) — the current `MarketplaceApp.tsx`
|
||||
(no agent-search at all, install state local to each card) and `CapabilityRequestCard` (local
|
||||
`useState` phase) have **independent** state. This is the headline PR4 frontend build; agent-pick is
|
||||
one of the three consumers of that store. (Owned by the "shared install store" slice — flagged here
|
||||
as the integration boundary.)
|
||||
|
||||
8. **`MarketplaceApp.tsx` has no agent-search UI.** Verified: the 417-line component
|
||||
(`apps/web/src/components/os/apps/MarketplaceApp.tsx`) contains no "Ask the agent" / suggestion /
|
||||
`acquire`-style references — only grid install/uninstall via `installMarketplacePackage` (:228).
|
||||
Variation A's centered bar + suggestion box must be built net-new.
|
||||
|
||||
9. **Marketplace FTS rank not surfaced.** `searchMarketplace` hardcodes `score: undefined`
|
||||
(index.ts:651), so marketplace candidates rank purely by keyword re-scoring inside
|
||||
`searchCapabilities`. Acceptable, but means FTS relevance is currently dropped on the floor for the
|
||||
agent-pick path.
|
||||
|
||||
---
|
||||
|
||||
## Exact integration points a PR4 build would touch
|
||||
|
||||
- **Reuse (engine):** `searchCapabilities(input): AcquisitionProposal`
|
||||
— `packages/agent/src/capability-acquisition.ts:179`. Exported from `@waggle/agent`
|
||||
(`packages/agent/src/index.ts:326`). Types `CapabilityCandidate` / `AcquisitionProposal` /
|
||||
`MarketplaceCandidate` / `SearchCapabilitiesInput` are all exported.
|
||||
- **Build (route):** new `POST /api/marketplace/agent-search` (body `{ need }`) in the marketplace
|
||||
route file `packages/server/src/local/routes/marketplace.ts` — runs `searchCapabilities` with deps
|
||||
assembled like `local/index.ts:620–657`, returns structured `{ candidates, recommendation,
|
||||
groupedByKind }`. Pattern to mirror for shape/contract: `GET /api/skills/suggestions`
|
||||
(`packages/server/src/local/routes/skills.ts:393`).
|
||||
- **Reuse (deps):** marketplace search `MarketplaceDB.search({query,limit})`
|
||||
(`packages/marketplace/src/db.ts:86`); installed-skills source `server.agentState.skills`; native
|
||||
tool-name union (currently only assembled at `local/index.ts:624` — lift if reused).
|
||||
- **Add (grouping):** a `pickOnePerKind(candidates)` helper (new) to satisfy connector+skill+tool —
|
||||
trivial reduce over `candidate.type`.
|
||||
- **Install actions (already present, reuse):** `adapter.installMarketplacePackage` /
|
||||
`adapter.installPack` / `adapter.connectConnector` / `adapter.installMcp`
|
||||
(`apps/web/src/lib/adapter.ts:1338/1292/1971/2017`). Marker/card render reuse:
|
||||
`segmentText` + `CapabilityRequestCard` (chat-blocks/).
|
||||
- **Count bar:** `SearchResult.installedCount` (`packages/marketplace/src/types.ts:242`) via
|
||||
`/api/marketplace/search`; needs workspace-scoping if §09's per-workspace count is taken literally.
|
||||
- **Audit (existing, ride along):** `fastify.auditStore?.record(...)` already called on gap in
|
||||
skill-tools.ts:461 — a route should record proposals the same way.
|
||||
|
||||
---
|
||||
|
||||
## Risks / watch-outs
|
||||
|
||||
- **Two parallel matchers + a router** (`searchCapabilities` vs `SkillRecommender` vs
|
||||
`CapabilityRouter`) — building a new route on the wrong one (e.g. `SkillRecommender`, which is
|
||||
skills-only and HTTP-exposed already) would silently drop connectors/MCP. **Use
|
||||
`searchCapabilities`.**
|
||||
- **`summary` vs structured-candidates confusion** — the tool's return value is markdown; do not parse
|
||||
it for the UI. Return the proposal object from the new route.
|
||||
- **Keyword-only matching** — no embeddings; verbose/synonym-heavy needs may under-match. Acceptable
|
||||
for v1 but the suggestion box may look thin on phrasing mismatch. (`SkillRecommender` has synonym
|
||||
expansion; `searchCapabilities` does not.)
|
||||
- **Tier gating asymmetry** — marketplace/MCP install is PRO-gated (marketplace.ts:181 `requireTier`),
|
||||
starter-pack/skill is free. The suggestion box must reflect this (the inline card already 403→Upgrade,
|
||||
CapabilityRequestCard.tsx:55).
|
||||
- **Workspace scoping of installs** — marketplace DB is global; the "N in this workspace" framing may
|
||||
over-promise isolation that the substrate doesn't provide.
|
||||
- **Native tool-name list is closure-local** (index.ts:624) — a route reconstructing deps must not
|
||||
drift from the real registered tool set, or agent-pick "already have a tool" answers go stale.
|
||||
|
||||
---
|
||||
|
||||
## Open questions for the founder/lead
|
||||
|
||||
1. **Route shape:** dedicated `POST /api/marketplace/agent-search`, or extend
|
||||
`GET /api/marketplace/search` with an `agentPick=true` mode? (The former is cleaner given the verbose
|
||||
NL body + structured proposal response.)
|
||||
2. **One-of-each-kind vs top-N:** §09 shows exactly connector+skill+tool (3). When a kind has no match
|
||||
(e.g. no relevant connector), show 2? Show an empty-kind hint? Define the grouping contract.
|
||||
3. **Count bar semantics:** is "N in this workspace" literally per-workspace (needs new
|
||||
workspace-scoped install tracking) or is catalog-wide `installedCount` acceptable for v1?
|
||||
4. **Suggestion-box "why":** use the engine's `matchReason` (keyword-hit-grade, e.g. "name matches:
|
||||
risk") as-is, or have the route pass candidates to the LLM for a one-line natural "why"? The former
|
||||
is free + deterministic; the latter is prettier but adds a model call.
|
||||
5. **Shared install store ownership:** confirm the frontend "sync" store is a separate PR4 slice that
|
||||
agent-pick plugs into (this recon treats it as the integration boundary, not part of Slice 5).
|
||||
40
docs/redesign-warm-hive/pr4-recon/GROUNDING-2026-06-16.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# PR4 Grounding — recon verified against live code (2026-06-16)
|
||||
|
||||
5-reader workflow `wf_19393d8b-c2d` (~669k tokens). Verdicts: Slice1 minor-drift · Slice2 confirmed · Slice3 confirmed · Slice4 **MAJOR-drift** · Slice5 minor-drift. **Net: the build plan is sound; the recon DOCS had 2 material errors that change Phases C/D execution (not the architecture).**
|
||||
|
||||
## Material drifts (these change HOW a phase is built)
|
||||
|
||||
### D-A (Slice 4, MAJOR) — Phase D binds to `CapabilityRequestCard`, NOT the approval gate
|
||||
Recon doc `04-inline-in-chat.md` pointed Phase D at `ApprovalGate`/`InlineApprovalCard` (the SSE `approval_required` singleton). **Wrong.** The live inline-install card is **`chat-blocks/CapabilityRequestCard.tsx`**, parsed from agent TEXT by `capability-request-parser.segmentText` (marker `<!--waggle:capability_request {json}-->` or legacy phrasing) and rendered by `TextBlock.tsx:25-27`. The build plan §2/§5 already names CapabilityRequestCard correctly — so the PLAN is right, only recon-04's framing was misleading. **Phase D = (1) widen `CapabilityRequest.kind` `'skill'|'marketplace'` → `+'connector'|'mcp'` (parser already passes `kind` through verbatim — parser.ts:22 — no parser change to ACCEPT, only to RENDER/DISPATCH); (2) add connector/mcp dispatch branches in `CapabilityRequestCard.handleInstall` routing through the shared store; (3) connector approve is FE-direct `adapter.connectConnector(id,{token})` — token NEVER transits the boolean approval wire (`PendingApproval.resolve(approved:boolean)` — index.ts:168). Do NOT build a 3rd ConnectorOfferCard.** Marker regex `(\{[^}]+\})` forbids nested `}` — fine, a connector offer is flat `{name,source,kind,reason}`.
|
||||
|
||||
### D-B (Slice 5, minor but load-bearing) — `searchCapabilities` has NO connector/mcp lane
|
||||
`searchCapabilities()` only ever EMITS `type ∈ {'native','skill','marketplace'}` — never `'connector'|'mcp'|'plugin'` (truth table below). ALL marketplace results collapse to `type:'marketplace'` because the `searchMarketplace` closure (index.ts:646) drops `waggle_install_type`. Connectors aren't even an input (they live in `connectorRegistry`, not marketplace.db). **So §09's literal "connector + skill + tool" three-up box CANNOT be produced by `pickOnePerKind(candidate.type)`.** → **PHASE-C DECISION (founder-gate when I reach C):** either (a) **honest scope** — group on what the engine really emits: *native-tool + skill + marketplace-pkg*, each with real `matchReason` "why" + `installAction` (delivers a true three-up box, no connector lane); or (b) **add a connector lane** — keyword-match `connectorRegistry.getDefinitions()` in the route and inject connector candidates + carry `waggle_install_type` through `MarketplaceCandidate` so mcp/skill packages bucket correctly (scope+). Recommend (a) for v1; flag (b) as the "literal §09" upgrade.
|
||||
|
||||
## Verified contracts (verbatim — bind to these)
|
||||
|
||||
### Phase A — shared install store (UNAFFECTED by drifts; fully grounded, build now)
|
||||
- **No FE store exists** (grep InstallProvider/useInstallStore/InstallStore → none). Providers pattern = React context (`ServiceProvider`/`ShellContext`/`ThemeProvider`), no Redux/Zustand. `useService()` exposes `{connecting}`; gate authed hydrate on connect-settled (MarketplaceApp.tsx:101).
|
||||
- **Value type + normalizers REUSE** `apps/web/src/lib/extension-catalog.ts`: `Extension{id,name,description,type,source,installed,lifecycle:'available'|'installed',installable,kind:'package'|'pack'|'federated',packageId?,scanStatus?,trust?,category?,openIn?}`. ID scheme keys the store: `pkg:<n>|pack:<slug|id|name>|connector:<id>|agent:<id>|model:<id>|template:<id>|mcp:<id>`. `installable` is hard-coded FALSE today for connector/mcp/pack → Phase B flips connector/mcp to true once the store can install them.
|
||||
- **Install verbs (adapter, REUSE):** skill `installSkill(id, source:'starter'|'pack'|'marketplace', packageId?)` | starter `installPack(skillId):void` | marketplace pkg `installMarketplacePackage(packageId:number):Promise<Response>` (RAW — inspect .status/.json; 403 TIER fires `waggle:tier-insufficient`, 403 `{blocked}`, 422 fail) | connector `connectConnector(id,{token?|apiKey?...}):void` (UNGATED) | mcp `installMcp(mcpId,opts?):{installed,server?,status?,requiresApproval?}` (PRO).
|
||||
- **Hydrate reads:** `GET /api/skills` {skills:[{name,...}],count} · `GET /api/connectors` {connectors:ConnectorDefinition[]} (status derived live: connected/disconnected/expired/error) · `GET /api/mcps` {mcps:McpListItem[],total,installed} (per-row installed/status/scope) · `GET /api/marketplace/installed` {installations,total} + `/search` per-row `installed` flag (the ONLY queryable installed-set; skills/connectors/mcp derive at read).
|
||||
- **Reconcile hazard:** install can 403(TIER)/403(SecurityGate)/422 AFTER an optimistic flip → store rolls back + routes to upgrade(`waggle:tier-insufficient`)/approval; count bar must never show a gate-rejected item.
|
||||
- **Count bar (D1 ratified):** FE-derived GLOBAL count (install_audit has NO workspace_id, is append-only + OSS-excluded; `getInstalledCount()` is global). Label honestly "installed", not "in this workspace".
|
||||
|
||||
### Phase B — Variation A grid
|
||||
- `ExtensionCard` props `{ext, installing?, onInstall?(ext), onUninstall?(ext), onOpenIn?(appId)}`; testids root `extension-card`, install `extension-install-${ext.id}`. Today: generic Install/Remove/Open-in, no type-aware verbs, no micro-states. Phase B adds Add/Connect/Enable + idle→in-progress→done from the store.
|
||||
- `MarketplaceApp` exports (named) `installRiskFor`/`buildRemoveRequest`/`buildInstallRequest` + default. Facets today = `['all',...EXTENSION_TYPES]` = 7. D2 collapse → 4 (All/Skills/Connectors/MCP). Count today = `visible.length` → replace with store count bar.
|
||||
- **Tests to rewrite = 10 `it()` blocks** (recon said 11 — DRIFT) in `apps/web/src/test/phase4b-marketplace-extend.test.tsx`; adapter mocked via `vi.hoisted`. Pinned testids: extension-facets, federated-note, extension-card, extension-install-pkg:7/:9, approval-modal(-approve); call shapes `getMarketplace({type:'skill',limit:30})`/`({type:'mcp',limit:30})`.
|
||||
- **Path correction:** ExtensionCard lives at `components/os/apps/extend/ExtensionCard.tsx` (nested under apps/), NOT `os/extend/`.
|
||||
|
||||
### Phase C — POST /api/marketplace/agent-search (route under `packages/server/src/local/routes/`, NOT `src/routes/`)
|
||||
- Engine `searchCapabilities(input:SearchCapabilitiesInput):AcquisitionProposal` exported from `@waggle/agent` (index.ts:326). Input `{need, installedSkills:[{name,content}], starterSkillsDir, nativeToolNames?, marketplaceCandidates?}` — engine does NO marketplace IO (candidates passed in).
|
||||
- `CapabilityCandidate{name,type,availability,description,source,matchScore,matchReason,installAction,trust?}`. **Emitted-type truth table:** native→`type:'native',availability:'active',installAction:null`; active skill→`'skill','active',null`; starter→`'skill','installable','install_capability'`; marketplace→`'marketplace','installable','install_capability'`. `recommendation` = SINGLE candidate; `candidates` capped at 8; `summary` is markdown for the model — **return STRUCTURED candidates/recommendation, NOT summary** (skill-tools.ts:476 discards it).
|
||||
- **Deps to lift (index.ts:620-657):** `nativeToolNames` = union of mind/system/plan/git/document tool names (closure-local; OMITS search/browser/cli/cron/connector — so web_search can't score even today); `getInstalledSkills` = `server.agentState.skills ?? loadSkills`; `searchMarketplace` = `marketplaceDb.search({query,limit:10}).packages → MarketplaceCandidate` (drops waggle_install_type + score). Ride-along audit `fastify.auditStore?.record({...action:'proposed',initiator:'agent'})`.
|
||||
|
||||
### Phase D — inline (see D-A above for the corrected target)
|
||||
- Boolean approval channel CONFIRMED: `PendingApproval.resolve:(approved:boolean)` (index.ts:168); `POST /api/approval/:requestId` body `{approved,always?,reason?,sourceWorkspaceId?}` — NO token. `adapter.respondApproval(requestId,approved,opts)`.
|
||||
- Connect route `POST /api/connectors/:id/connect` body `{token?|apiKey?,refreshToken?,expiresAt?,scopes?,email?}` → vault `setConnectorCredential`; 404/400/503; authType = `registry.get(id)?.authType ?? 'bearer'`. **Card must branch on authType** — token-paste fits bearer/api_key only; OAuth (oauth2) needs redirect → Hub fallback (D3). Google family connector-id ≠ provider key (`OAUTH_PROVIDER_FOR_CONNECTOR`).
|
||||
- Gate a new agent offer/connect tool: add exact name to `ALWAYS_CONFIRM` (confirmation.ts:16) OR name it `connector_<id>_<writeverb>_*` (CONNECTOR_WRITE_PATTERNS).
|
||||
|
||||
## install_audit (do not touch as state)
|
||||
Append-only INSERT-only, NO `workspace_id`, action vocab = `proposed|approved|installed|rejected|failed|blocked|uninstalled` (NO 'synced'/'connected' — adding one is a CHECK migration). OSS-EXCLUDED (interleaved in hive-mind-core schema.ts/db.ts; curated out by hand) → install_audit-only changes have nowhere to land on the mirror. NEVER the source of installed state.
|
||||
116
docs/redesign-warm-hive/pr7-recon/01-stripe-backend.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# PR7 Recon · 01 — Stripe / Billing BACKEND state
|
||||
|
||||
**Scope:** What is REAL vs MUST-BUILD for the 4 billing screen states (Plans / Checkout / Success / Manage) of **screen 14 (Billing)** — plus the auth-identity context that screen 13 (Auth) and "Manage" lean on.
|
||||
**Method:** read + grep only, no product code changed. Every claim cites `file:line`.
|
||||
**Target surface:** PR7 builds in **`apps/web`** (the React desktop cockpit) → talks to the **local Fastify sidecar** (`packages/server/src/local/index.ts`). This is a DIFFERENT server entry from the cloud/`apps/www` stack, and that distinction is the single most important fact below.
|
||||
|
||||
---
|
||||
|
||||
## 0. The two-stack split (read this first)
|
||||
|
||||
There are **two** server entry points and **two** Stripe/auth integrations. They must not be conflated:
|
||||
|
||||
| | **Local sidecar** (PR7 target) | **Cloud server / `apps/www`** (PR8 / not PR7) |
|
||||
|---|---|---|
|
||||
| Entry | `packages/server/src/local/index.ts` | cloud: `packages/server/src/index.ts`; landing: `apps/www/app/**` |
|
||||
| Tier source | `config.json` on disk (`assert-tier.ts:21-32`) | Clerk `publicMetadata` + cloud DB |
|
||||
| Auth | **none** — no Clerk plugin registered in `local/` (grep: 0 hits for `plugins/auth` in `src/local`) | Clerk: `packages/server/src/plugins/auth.ts` (only imported by `src/index.ts`); `apps/www` uses `@clerk/nextjs` |
|
||||
| Stripe routes | `packages/server/src/stripe/**` (checkout/portal/sync/webhook) | `apps/www/app/api/stripe/checkout/route.ts` + `app/api/webhooks/stripe/route.ts` |
|
||||
| Stripe ↔ identity link | customer id stored in **`config.json` `stripe_customer_id`** (`webhook.ts:60`, `portal.ts:28`) | customer id stored in **Clerk `publicMetadata.stripeCustomerId`** (`apps/www/.../checkout/route.ts:94-97`) |
|
||||
|
||||
`apps/web` consumes the **local sidecar** (adapter base = local URL; `apps/web/src/lib/adapter.ts` calls `/api/stripe/*`). So **PR7's backend is the `packages/server/src/stripe/**` set, NOT the richer `apps/www` Clerk-linked flow.** The `apps/www` flow (full Clerk identity + lazy Stripe Customer + lookup_key price resolution) is PR8 territory and should not be assumed available in the desktop cockpit.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stripe backend inventory (local sidecar — the PR7 surface)
|
||||
|
||||
All routes registered via `stripeRoutes` at `packages/server/src/local/index.ts:128,2161`. All gate on `STRIPE_SECRET_KEY`; absent → **503 `STRIPE_NOT_CONFIGURED`** (`index.ts:25-43`, each route).
|
||||
|
||||
| Route | File:line | What it does | REAL? |
|
||||
|---|---|---|---|
|
||||
| `POST /api/stripe/create-checkout-session` | `checkout.ts:16` | Hosted Stripe Checkout session (`mode:'subscription'`), PRO/TEAMS only, period-aware price, returns `{url}` | **REAL** |
|
||||
| `POST /api/stripe/create-portal-session` | `portal.ts:15` | Stripe **Customer Portal** session, `requireTier('PRO')` gated, needs `config.json.stripe_customer_id`, returns `{url}` | **REAL** |
|
||||
| `POST /api/stripe/sync` | `sync.ts:21` | Poll-confirm after redirect (desktop behind NAT), retrieves session, **payment-gated** (`status==='complete' && paid`, `sync.ts:46-49`), writes tier to config | **REAL** |
|
||||
| `POST /api/stripe/webhook` | `webhook.ts:74` | Signature-verified lifecycle handler — **flips tier in config.json** | **REAL** |
|
||||
| `GET /api/tier` | `settings.ts:322` | Authoritative tier read (+ `trialDaysRemaining`, `trialExpired`) | **REAL** |
|
||||
| `PATCH /api/tier` | `settings.ts:363` | Dev/testing tier override ("will be replaced by Stripe webhook") | REAL (dev) |
|
||||
| `POST /api/tier/start-trial` | `settings.ts:401` | Atomic 15-day trial start | REAL |
|
||||
| **invoice list** | — | **does not exist** (grep `invoice`/`invoices.list` in `packages/server` → 0 product hits; only a chat-keyword at `chat-helpers.ts:15`) | **MUST-BUILD** |
|
||||
| **payment-method read/update** | — | **does not exist** (grep `paymentMethod`/`payment_method` → 0 hits) | **MUST-BUILD (or defer to Portal)** |
|
||||
|
||||
**Adapter methods already on the FE** (`apps/web/src/lib/adapter.ts`): `createCheckoutSession` (2667), `createPortalSession` (2675), `syncStripeCheckout` (2659), `getTier` (2682). The **`useBilling` hook already orchestrates the whole happy path** — `apps/web/src/hooks/useBilling.ts`: `startCheckout` opens the URL (69-81), `openPortal` opens the portal URL (84-96), `syncAfterCheckout` confirms (49-66), and it **auto-detects `?session_id=` on mount** to run sync (104-115). It also carries the honesty primitive `tierResolved` (16-21): until a real `getTier()` round-trip succeeds, the default `'FREE'` is a **placeholder, not a fact** — billing surfaces must render an unresolved state, not the FREE card.
|
||||
|
||||
### Webhook DOES flip tiers (REAL)
|
||||
`webhook.ts:114-158` handles three events, all writing tier to `config.json` via `updateUserTier` (52-62):
|
||||
- `checkout.session.completed` → grants tier **only if `payment_status` is `paid`/`no_payment_required`** (122) — unpaid sessions never grant.
|
||||
- `customer.subscription.updated` → re-resolves tier from price id via `tierFromPriceId` (139).
|
||||
- `customer.subscription.deleted` → **downgrades to FREE** (150).
|
||||
Plus real hardening: raw-body signature verification (88-95), idempotency via `.stripe-processed-events.json` + a serialized critical section against TOCTOU double-processing (40-45, 102-165), atomic temp-file writes (25-29). CLAUDE.md §10 (E-10) confirms 17/17 webhook tests green.
|
||||
|
||||
---
|
||||
|
||||
## 2. Price wiring / env contract (REAL, documented)
|
||||
|
||||
`tierFromPriceId` (`index.ts:71-87`) and `priceIdForTier` (`index.ts:91-100`) resolve a **dual env contract**:
|
||||
- **4-var** (matches `apps/www`): `STRIPE_PRICE_PRO_MONTHLY` / `_PRO_ANNUAL` / `_TEAMS_MONTHLY` / `_TEAMS_ANNUAL`.
|
||||
- **legacy single-var** fallback: `STRIPE_PRICE_PRO` / `_TEAMS` / `_BASIC`(→PRO).
|
||||
- final fallback: `TIER_CAPABILITIES[tier].stripePriceId` (`tiers.ts:122,143`, read from `STRIPE_PRICE_PRO`/`_TEAMS` env).
|
||||
|
||||
CLAUDE.md §10 (M7) records both **test (`acct_1SzHlbC0mmjh4oEM`) and live (`CNCrMQy1f7`)** Stripe accounts hold 2 products × 2 prices with lookup_keys `pro_monthly`/`pro_annual`/`teams_monthly`/`teams_annual`; live price IDs in `docs/launch/drafts/2026-05-12-apps-www-deployment-readiness.md`. **The price IDs/secret are EXTERNAL-DEP** (must be present in the sidecar's env at runtime). Pricing on the screen (Solo $0 / Pro $19 / Teams $49-seat, `SCREENS.md:294-295`) is **DERIVABLE** from `tiers.ts` doc-comment (`tiers.ts:7-12`) — but the literal dollar amounts are NOT machine-readable fields in `TIER_CAPABILITIES`; only `stripePriceId` is. So plan-card prices are static copy unless cross-checked against the Stripe dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-state verdict (screen 14 · Billing)
|
||||
|
||||
Screen spec: `SCREENS.md:282-295`. Note the explicit instruction: **"Use Stripe Checkout/Customer Portal where possible; theme to tokens."**
|
||||
|
||||
| State | Backend verdict | Evidence / what's needed |
|
||||
|---|---|---|
|
||||
| **Plans** (monthly/annual toggle, 3 cards) | **REAL (read) + DERIVABLE (copy)** | tier from `GET /api/tier` (`settings.ts:322`); current-plan highlight from `useBilling.tier` + `tierResolved`; prices are static copy derivable from `tiers.ts:7-12`. Upgrade buttons call existing `createCheckoutSession`. No new backend. |
|
||||
| **Checkout** (custom card form: email, card 4242, expiry/CVC, country) | **MUST-NOT-BUILD as custom; redirect to HOSTED Checkout (REAL)** | The screen mock shows a **custom card form** (`SCREENS.md:287-289`), but the backend only produces a **hosted Stripe Checkout URL** (`checkout.ts:39-51`). There is **no card-tokenization / PaymentElement / Stripe.js** anywhere in `apps/web` (grep `CardElement`/`PaymentElement`/`4242` → 0 hits). **PCI/scope flag below.** Recommended: the "Checkout" segment is a themed **summary/preview that hands off to hosted Checkout**, not a real PAN field. |
|
||||
| **Success** ("You're Pro", receipt, Manage) | **REAL** | Already wired: `useBilling` auto-runs `syncStripeCheckout` on `?session_id=` (`useBilling.ts:104-115`); sync is payment-gated (`sync.ts:46-49`). The post-redirect `/payment-success` URL is set at `checkout.ts:42`. "Receipt" line-items are **NOT returned by sync** (`sync.ts:83` returns only `{tier, customerId}`) → a real receipt would need a new fetch or Portal link → **MUST-BUILD or show generic confirmation**. |
|
||||
| **Manage** (current plan, switch annual, payment method ···4242, next charge, **invoices PDF**, change/cancel) | **PARTLY REAL via Portal; the in-app detail is MUST-BUILD** | `createPortalSession` (`portal.ts:15`) gives a one-click jump to Stripe's **hosted Customer Portal**, which natively does payment-method update, invoice PDFs, plan change, cancel. **BUT** rendering those *inside* the cockpit (the "VISA ···4242", "next charge", invoice list with Paid+PDF in the mock, `SCREENS.md:291-292`) requires routes that **do not exist**: no invoice-list, no payment-method read, no subscription-detail route on the local sidecar. Also Portal requires `config.json.stripe_customer_id` to already be set (only written after a real paid checkout/webhook, `webhook.ts:60`) else **400 `NO_STRIPE_CUSTOMER`** (`portal.ts:32-34`). |
|
||||
|
||||
---
|
||||
|
||||
## 4. Auth context for screen 13 (and what "Manage" identity rests on)
|
||||
|
||||
Screen 13 spec (`SCREENS.md:269-278`): "Build with **Clerk** components themed to the tokens," with honest "account is optional / local-first" framing.
|
||||
|
||||
- **In `apps/web` (PR7 target): Clerk is NOT present.** grep `@clerk`/`ClerkProvider`/`useUser`/`signIn` in `apps/web` → **0 files**. The local sidecar registers **no auth plugin** (grep `plugins/auth` in `src/local` → 0). So **the desktop has no real logged-in identity** — tier lives in `config.json`, not behind a session.
|
||||
- **Clerk IS wired, but only in the OTHER stack:** root dep `@clerk/fastify` (`package.json:45`) is consumed by `packages/server/src/plugins/auth.ts` (verifyToken + auto-provision) and `ws/gateway.ts:3` — both reachable **only from the cloud entry `src/index.ts`**, not the desktop sidecar. `apps/www` has the full Next.js Clerk surface (`sign-in`, `sign-up`, `account/page.tsx` using `<UserProfile>`, `@clerk/nextjs ^7.3.0` + `@clerk/themes ^2.4.57` in `apps/www/package.json:15-16`).
|
||||
- **Therefore screen 13 in the desktop is EXTERNAL-DEP + design decision, not a wiring task.** Either (a) embed Clerk in `apps/web` for the first time (new provider, new keys, new session model — large, and contradicts "local-first / account optional"), or (b) make screen 13 a **themed informational/SSO-handoff** screen that links to `apps/www` Clerk and keeps the desktop accountless. The honesty contract leans hard toward (b): **do not render a logged-in identity (name/email/avatar) the desktop does not actually have.** (PR1 already flagged the `userName={null}` sidebar row, BUILD-PLAN.md:189.)
|
||||
|
||||
---
|
||||
|
||||
## 5. FABRICATION RISKS — must be gated off, never invented
|
||||
|
||||
1. **Invoices list (Manage).** No invoice route exists. A static "Invoice #1234 · Paid · PDF" list would be **fabricated billing history**. Gate: only show invoices if a real route is built against `stripe.invoices.list(customer)`; otherwise **link out to the hosted Portal** for invoices. (`SCREENS.md:292`)
|
||||
2. **Payment method "VISA ···4242".** No payment-method route. The mock's "···4242" is literally Stripe's test PAN. Hardcoding it = **fake payment method**. Gate: render only from a real `paymentMethods.list`, else Portal-only. (`SCREENS.md:291`)
|
||||
3. **"Next charge" / billing-cycle date.** Not returned by any local route (`sync.ts:83`, `getTier`). Inventing a date = fabrication. Gate: derive from a real subscription fetch or omit.
|
||||
4. **Custom card form (4242 PAN field).** A real-looking PAN/CVC field that doesn't tokenize would be both fake AND a PCI-scope trap (see §6). Gate: never collect raw PAN in-app; hand off to hosted Checkout.
|
||||
5. **Logged-in identity on Auth / sidebar (screen 13).** Desktop has no Clerk session. Showing a real name/email/avatar = fabricated identity. Gate: keep accountless or SSO-handoff; the `tierResolved`-style "unresolved" pattern (`useBilling.ts:16-21`) is the precedent.
|
||||
6. **Tier shown as FREE before resolution.** `useBilling` already guards this with `tierResolved` (`useBilling.ts:36-46`) — the Plans "current plan" badge must honor it, not assume FREE.
|
||||
7. **Receipt on Success.** `syncStripeCheckout` returns no receipt/amount (`sync.ts:83`). A "$19 charged" receipt line would be invented. Gate: generic "You're Pro" confirmation, or Portal link, until a real receipt fetch exists.
|
||||
|
||||
---
|
||||
|
||||
## 6. Custom card form vs hosted — PCI / scope implication (FLAG)
|
||||
|
||||
The screen mock shows a **custom card form** (email, card 4242, expiry/CVC, name, country — `SCREENS.md:287-289`), but the **backend only emits a hosted Stripe Checkout URL** (`checkout.ts:39-51`) and the screen note itself says **"Use Stripe Checkout/Customer Portal where possible"** (`SCREENS.md:294`).
|
||||
|
||||
**Implication:** A real custom PAN form means the card number touches the app's DOM → **PCI-DSS scope jumps from the trivial SAQ-A (hosted/redirect) to SAQ-A-EP or higher**, and would require Stripe.js Elements / PaymentElement client-side tokenization (none exists in `apps/web` today — grep confirms 0). For a Tauri desktop binary this is a material compliance + security burden for zero functional gain over the already-built hosted flow.
|
||||
|
||||
**Recommendation (for the build-plan decision):** treat the Checkout-segment card UI as a **themed visual preview/order-summary that redirects to hosted Stripe Checkout** (reuse `createCheckoutSession`), and treat "Manage" detail (payment method, invoices, cancel) as a **themed launchpad to the hosted Customer Portal** (reuse `createPortalSession`). Build new local routes (invoice-list / subscription-detail) ONLY if founder wants those rendered in-app — and even then, render strictly from live Stripe data, never placeholders.
|
||||
|
||||
---
|
||||
|
||||
## 7. One-line summary per question asked
|
||||
|
||||
- **create-checkout-session route?** YES — REAL (`checkout.ts:16`).
|
||||
- **customer-portal route?** YES — REAL (`portal.ts:15`), but needs `stripe_customer_id` in config first.
|
||||
- **invoice-list route?** NO — MUST-BUILD (or defer to hosted Portal).
|
||||
- **Does the webhook flip tiers?** YES — REAL, payment-gated, idempotent (`webhook.ts:114-158`).
|
||||
- **What does "Manage" need?** Either reuse the hosted Portal (REAL today) OR build 3 new local routes: invoice-list, payment-method, subscription-detail (none exist).
|
||||
- **Hosted vs custom?** Hosted is the intended + already-built path; the mock's custom 4242 form is a PCI-scope trap — flag and prefer hosted.
|
||||
- **Auth screen 13 backend?** Desktop has NO Clerk/identity (EXTERNAL-DEP + design call); Clerk only lives in the cloud server + `apps/www`.
|
||||
129
docs/redesign-warm-hive/pr7-recon/02-auth-session.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# PR7 Recon · 02 — Current auth / session / identity model
|
||||
|
||||
> Recon-only. No product code touched. Every claim cites `file:line`.
|
||||
> Scope: the substrate **PR7 Auth (screen 13, Clerk) and Billing (screen 14, Stripe)** must reconcile with.
|
||||
> Honesty contract (PR3–PR6): every place PR7 could fabricate a logged-in identity, fake invoices/usage/payment methods is flagged for gate-off.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR — the architecture tension, stated plainly
|
||||
|
||||
Waggle today has **TWO unrelated "auth" systems**, and PR7's screen 13 belongs to neither cleanly:
|
||||
|
||||
1. **The local sidecar (what `apps/web` actually talks to)** authenticates with a **per-process random bearer token** — NOT a user login. There is **no account, no email, no password, no Clerk** on this path. "Who the user is" = a free-text **name** typed into onboarding (IdentityLayer), and "what tier" = a `tier` string in `config.json`. The local app runs **fully without any account** — this is literally true today, which matches the design's "an account is optional — Waggle runs fully local without one."
|
||||
2. **A separate cloud/team Fastify server** (`packages/server/src/{plugins,routes,services,db}/`, distinct from `packages/server/src/local/`) **does** use **Clerk** (`@clerk/fastify`) with a real `users` DB table keyed by `clerkId`. This is the TEAMS/cloud-sync path — it is gated by `CLERK_SECRET_KEY` and is **not wired into the desktop `apps/web` UI at all**.
|
||||
|
||||
**Implication for PR7 Auth (screen 13):** there is **no existing login/signup UI in `apps/web`** and no client-side Clerk dependency. A "Sign in with Clerk" screen is a **MUST-BUILD net-new surface + an EXTERNAL-DEP** (Clerk publishable key + a decision about which server validates the session). The honest framing the design already calls for ("account optional, local-first") is not just copy — it is the actual current architecture, and PR7 must not regress it into a hard auth wall.
|
||||
|
||||
**Implication for PR7 Billing (screen 14):** the **entire Stripe flow already exists and is wired client→server** (checkout, portal, sync, tier read, trial). Billing is mostly **REAL/DERIVABLE** — the work is a themed UI over hooks that already work. The fabrication risk is the design's mocked **invoices / payment-method / "VISA ···4242"** content, which is NOT in any current API.
|
||||
|
||||
---
|
||||
|
||||
## 1. How the app authenticates TODAY (no account)
|
||||
|
||||
### 1.1 The token is a per-process secret, minted at sidecar boot — not a credential
|
||||
- `packages/server/src/local/index.ts:1409` — `wsSessionToken: crypto.randomBytes(32).toString('hex')` is generated once when the sidecar's agent state is created. It is **process-lifetime**, tied to nothing about a user.
|
||||
- `packages/server/src/local/index.ts:2039–2041` — registered into the security middleware as `sessionToken: server.agentState.wsSessionToken`.
|
||||
- `packages/server/src/local/index.ts:2047–2051` — `GET /api/auth/session-token` returns `{ token: wsSessionToken }`, **auth-exempt** but **same-origin gated** via `isLocalRequest(request)` (cross-origin → 403). This is the bootstrap: the webview reads the token once, then sends it as a Bearer on every other call.
|
||||
|
||||
### 1.2 Server enforcement = "is this the current process's token?", nothing about identity
|
||||
- `packages/server/src/local/security-middleware.ts:238` — `AUTH_EXEMPT_PATHS = ['/health', '/api/auth/session-token']`.
|
||||
- `:340–377` — bearer check: any non-exempt `/api/*` request must carry `Authorization: Bearer <sessionToken>` or it 401s with `MISSING_TOKEN`/`INVALID_TOKEN`. There is **no user lookup** — token equality is the whole check.
|
||||
- `:296–298` — D1: localhost is **no longer trusted by default** (`WAGGLE_TRUST_LOCALHOST=1` is the escape hatch) — because the desktop coexists with browsers/other local apps; the bearer token is what prevents any local process from driving the API.
|
||||
- `:343–353` — non-API GETs (the SPA shell + static assets) load token-less (chicken-and-egg bootstrap); every `/api/*` and non-GET stays gated.
|
||||
- `:249–254`, `:361–366` — SSE streams accept the same token via `?token=` (EventSource can't set headers). Still the same per-process token.
|
||||
|
||||
### 1.3 The client side: attach token, refresh on 401, never a login
|
||||
- `apps/web/src/boot-connect.ts:16–18` — `adapter.connect()` fires as `main.tsx`'s first import, arming the deferral gate before any component fetch.
|
||||
- `apps/web/src/lib/adapter.ts:298–308` — `fetchSessionToken()` GETs `/api/auth/session-token` on connect and stores `this.authToken` (best-effort).
|
||||
- `apps/web/src/lib/adapter.ts:444–446` — every non-exempt request attaches `Authorization: Bearer ${issuedToken}`.
|
||||
- `apps/web/src/lib/adapter.ts:316–337, 459–470` — on a 401 the adapter does ONE silent token refresh + retry (the token rotates every sidecar restart). **This is the only "session lifecycle" that exists** — it is process-rotation recovery, not user re-auth.
|
||||
- `apps/web/src/lib/adapter.ts:49` — client mirror of `AUTH_EXEMPT_PATHS`.
|
||||
|
||||
**There is no signin/signup/logout anywhere on this path.** (grep for `SignIn|SignUp|useAuth|LoginPage` across `apps/web/src` returns only `AppShell.tsx` (a tier label) and a test file — see §4.)
|
||||
|
||||
---
|
||||
|
||||
## 2. What currently consumes "who is the user"
|
||||
|
||||
| Consumer | Today's source | File:line | For PR7 |
|
||||
|---|---|---|---|
|
||||
| **Display name** (Home greeting, sidebar user row) | `IdentityLayer.name` — a free-text name typed in onboarding, stored per-mind in SQLite | `home.ts:261–270` (briefing `userName`); `AppShell.tsx:98,102,321`; `identity.ts:62–100` | A Clerk identity would *supersede* this name, but the IdentityLayer name is **not** an account |
|
||||
| **`HomeBriefing.userName`** (BUILD-PLAN §9 / PR1 LOW #2) | Same IdentityLayer name; optional, omitted when blank | `home.ts:406,411`; `adapter.getIdentity()` `adapter.ts:1090–1103` | This is the "user identity surface" the BUILD-PLAN points at — it is **identity, not auth** |
|
||||
| **Tier** (everything gated) | `config.json` `tier` field (single local user) | `assert-tier.ts:21–39` `readTierFromDataDir`; `settings.ts:309–333` `GET /api/tier` | Tier is **device-local**, not account-bound — Billing/Auth reconciliation point (§5) |
|
||||
| **Trial** | `config.json` `trialStartedAt`; effective tier downgrades TRIAL→FREE on expiry | `settings.ts:392–432`; `tiers.ts:191–204` | `startTrial` already exists client+server |
|
||||
| **Stripe customer** | `config.json` `stripe_customer_id` (written by webhook/sync) | `webhook.ts:52–62`; portal reads it | The billing "who" — a Stripe customer id, again device-local, **not** a Clerk user |
|
||||
| **Team identity (cloud only)** | Clerk `clerkId` → internal `users.id` UUID | `plugins/auth.ts:24–52`; `services/user-service.ts:12–58` | The ONLY place a real account identity exists today — and it's **not in `apps/web`** |
|
||||
|
||||
**Key reconciliation fact:** tier and Stripe customer live in **`config.json` on the local device, keyed to a single anonymous local user** (`assert-tier.ts:21`, `webhook.ts:52`). They are **not** keyed to a Clerk user id. If PR7 introduces a real Clerk login, the product must decide whether tier/billing stay device-local (today's model) or migrate to account-bound (the cloud server's model). This is unresolved and is the core architectural decision PR7 surfaces.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Clerk substrate that DOES exist (cloud/team server — not apps/web)
|
||||
|
||||
- `package.json:45` — `@clerk/fastify": ">=3.1.16 <4"` (root dep).
|
||||
- `apps/www/package.json:15–16` — `@clerk/nextjs": "^7.3.0"` + `@clerk/themes": "^2.4.57"` — but `apps/www` is the **landing page (Next.js), which is PR8**, not PR7's `apps/web` screen.
|
||||
- `apps/web/package.json` — **NO Clerk dependency** (verified, grep returns nothing).
|
||||
- `packages/server/src/plugins/auth.ts:3,21,31` — `createClerkClient` + `verifyToken`; auto-provisions an internal user from Clerk JWT claims on first auth (`:37–48`).
|
||||
- `packages/server/src/services/user-service.ts` — `users` table CRUD keyed by `clerkId`; `upsertFromClerk`.
|
||||
- `packages/server/src/local/security-middleware.ts:307–309` — **the local sidecar already references Clerk indirectly**: `const isTeamMode = !!process.env.CLERK_SECRET_KEY;` enables the 30-min session-inactivity timeout **only in team mode**. So "Clerk present" is already the team/cloud signal even on the local server, but it currently only toggles a timeout — it never establishes a logged-in user on the local path.
|
||||
|
||||
**Where a real Clerk identity slots in (PR7 Auth screen 13):** the design says "Build with Clerk components themed to the tokens" (`SCREENS.md:277`). For `apps/web` that means **adding `@clerk/clerk-react` (or `@clerk/clerk-js`) net-new**, mounting `<SignIn/>/<SignUp/>` themed to the warm tokens, and then deciding what the Clerk session *does*: (a) cosmetic/optional account that seeds the IdentityLayer name + (for Teams) unlocks cloud sync, or (b) a real session token the cloud server validates. Today nothing in `apps/web` consumes a Clerk session, so **(a) is the lower-risk, local-first-preserving path** and matches the design's "account is optional" framing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Existing UI surfaces (what's there vs. what PR7 must build)
|
||||
|
||||
- **No Auth/Login/SignUp/Billing page exists in `apps/web`.** Glob `**/*{Auth,Login,SignIn,SignUp,Billing}*.tsx` → only `overlays/LoginBriefing.tsx`, which is the **"while you slept" overnight-work briefing overlay (SCREENS §05 hero), NOT authentication** (named "login" only because it shows on app open). Do not mistake it for an auth screen.
|
||||
- `apps/web/src/components/os/AppShell.tsx:263–269` — `tierLabel` ("Trial · 9d" / "Pro") for the sidebar user row.
|
||||
- `apps/web/src/components/os/AppShell.tsx:98–104,321` — `userName` from `adapter.getIdentity()`, degrades to "Account" when unconfigured. **This is the "user-identity surface lands (PR3)" hook the BUILD-PLAN PR1 LOW #2 deferred to** — it is fed by IdentityLayer, and PR7 Auth could optionally re-feed it from a Clerk profile.
|
||||
- `apps/web/src/components/os/overlays/UpgradeModal.tsx` (imported `AppShell.tsx:37`) + `LockedFeature.tsx` — existing tier-gate upgrade prompts; `AppShell.tsx:411,422` call `adapter.createCheckoutSession(...)` directly. So an upgrade entry point already exists; PR7 Billing screen 14 is the **dedicated Plans/Checkout/Success/Manage surface** these can route into.
|
||||
- `apps/web/src/test/p1b-authgate-surfaces.test.tsx` — tests the *adapter* auth gate (token/401), not a login UI.
|
||||
|
||||
---
|
||||
|
||||
## 5. Billing substrate — already REAL end-to-end (screen 14 is mostly a themed re-skin)
|
||||
|
||||
**Client (`apps/web`):**
|
||||
- `apps/web/src/hooks/useBilling.ts` — full hook: `refreshTier` (`:34`), `syncAfterCheckout` (`:49`), `startCheckout` (`:69` → opens Stripe URL in new tab), `openPortal` (`:84`), auto-detects `?session_id=` post-checkout redirect (`:104–115`). Critically `tierResolved` (`:18`) means the UI must **not** present the default `'FREE'` as fact until a real `getTier()` round-trip succeeds — an existing honesty guard PR7 must honor.
|
||||
- `apps/web/src/lib/adapter.ts:2658–2683` — `syncStripeCheckout`, `createCheckoutSession('PRO'|'TEAMS')`, `createPortalSession`, `getTier`; `:2742+` `startTrial`.
|
||||
|
||||
**Server (`packages/server/src/stripe/` + `local/routes/settings.ts`):**
|
||||
- `checkout.ts:13–57` — `POST /api/stripe/create-checkout-session` → **Stripe-HOSTED Checkout** redirect (`session.url`), success→`/payment-success?session_id=…`, cancel→`/payment-cancelled` (`:42–43`). Matches design "Use Stripe Checkout/Customer Portal where possible" (`SCREENS.md:294`).
|
||||
- `portal.ts` — `POST /api/stripe/create-portal-session` (Customer Portal) — covers the design's "Manage" state (payment method update / invoices / cancel) **for free**, no custom UI needed.
|
||||
- `webhook.ts:64–169` — `checkout.session.completed` / `customer.subscription.updated` / `customer.subscription.deleted` → writes `tier` (+ `stripe_customer_id`) into `config.json`. Idempotent + serialized (`:40–45,105–165`). Only grants tier when `payment_status==='paid'|'no_payment_required'` (`:120–124`).
|
||||
- `index.ts:71–100` — `tierFromPriceId` / `priceIdForTier` resolve the 4-var (`STRIPE_PRICE_PRO_MONTHLY/_ANNUAL`, `…TEAMS…`) + legacy contracts.
|
||||
- `settings.ts:321–333` — `GET /api/tier` authoritative tier (+ trial days remaining).
|
||||
- **All `/api/stripe/*` routes 503 `STRIPE_NOT_CONFIGURED` when `STRIPE_SECRET_KEY` is unset** (`index.ts:25–43`, `checkout.ts:17–20`) — so on a dev/local machine without keys, Billing must render an honest "not configured" state, not a fake checkout.
|
||||
|
||||
**Design-vs-build for screen 14:** the design's "Checkout: 2-col card form (card 4242…, expiry/CVC)" (`SCREENS.md:287–289`) is **NOT how the current backend works** — checkout is a hosted redirect, there is no card-form endpoint and no card data ever touches Waggle. PR7 should ship the **Plans** state (real, from `getTier` + tier table) + **Success** (real, from `syncAfterCheckout`) + **Manage** (real, via Customer Portal), and either (a) drop the inline card form in favor of hosted Checkout, or (b) build Stripe Elements net-new (larger scope, more PCI surface). Recommend (a) — it matches the existing wiring and the design's own "where possible" caveat.
|
||||
|
||||
---
|
||||
|
||||
## 6. Fabrication risks for PR7 (must gate off — never invent)
|
||||
|
||||
1. **A logged-in identity that isn't real.** The local app has no account. The sidebar/Home already degrade `userName` to "Account" when IdentityLayer is blank (`AppShell.tsx:98–104`). PR7 Auth must NOT show a fabricated "Signed in as …" when no Clerk session exists — show the optional/local-first state.
|
||||
2. **Fake invoices / receipts.** No invoice API exists anywhere (`webhook.ts` writes only tier + customer id; no invoice list endpoint). The design's "invoices (Paid + PDF)" (`SCREENS.md:292`) has **no data source** — either omit, or surface them only via the **Stripe Customer Portal** (which renders real invoices), never as in-app mock rows.
|
||||
3. **Fake payment method ("VISA ···4242").** No payment-method API in the sidecar. Must come from the Customer Portal or be omitted — never hardcoded.
|
||||
4. **Fake usage numbers on Billing.** Tier is real (`getTier`); any "X of Y used" must come from a real source or be omitted.
|
||||
5. **Presenting default `FREE` as the user's plan.** `useBilling.tierResolved` (`useBilling.ts:18`) exists precisely to prevent this — PR7 Billing must render the unresolved state while `!tierResolved`, not the FREE card.
|
||||
6. **A working checkout when Stripe is unconfigured.** All `/api/stripe/*` 503 without `STRIPE_SECRET_KEY` — PR7 must render an honest disabled/"not configured" state, not a clickable fake "Subscribe".
|
||||
7. **Auth gating local-first features behind a login.** The whole product runs token-only with no account today (§1). PR7 Auth must stay **optional** — wiring it as a mandatory gate would regress the local-first contract the design explicitly states.
|
||||
|
||||
---
|
||||
|
||||
## 7. REAL / DERIVABLE / MUST-BUILD / EXTERNAL-DEP summary
|
||||
|
||||
- **REAL** — Local bearer-token session + 401-refresh; per-process token; `GET /api/auth/session-token`; tier in `config.json` + `GET /api/tier` + trial; **entire Stripe checkout/portal/sync/webhook flow** + `useBilling` hook; IdentityLayer name → `userName`; existing `UpgradeModal`/`LockedFeature` upgrade entry points.
|
||||
- **DERIVABLE** — Billing **Plans** card grid (from tier table + `getTier`); **Success** state (from `syncAfterCheckout`); **Manage** (delegate to Stripe Customer Portal). Sidebar "Signed in / Account" display from `getIdentity()`.
|
||||
- **MUST-BUILD** — Themed Auth screen 13 UI (no login UI exists in `apps/web`); themed Billing screen 14 surface (no dedicated billing page exists). Decision logic for "what a Clerk session does on the local path."
|
||||
- **EXTERNAL-DEP** — **Clerk** for `apps/web` (publishable key + new `@clerk/clerk-react` dep; root has only `@clerk/fastify`, `apps/www` has `@clerk/nextjs`); **Stripe** keys (`STRIPE_SECRET_KEY` + price-id env vars + `STRIPE_WEBHOOK_SECRET`) — without them all billing routes 503.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open decisions PR7 must resolve (carry to the build plan)
|
||||
|
||||
1. **Does a Clerk login replace, supplement, or stay independent of the local token?** Today nothing in `apps/web` validates a Clerk session; the local token is what authorizes the API. Recommend: Clerk stays **optional/cosmetic + cloud-sync trigger**, local token remains the API authorizer — preserves local-first, lowest blast radius.
|
||||
2. **Tier/billing keying: device-local (`config.json`, today) vs. account-bound (cloud `users` table)?** This is DESIGN_POV §4 (BUILD-PLAN §7 item 5, flagged as the PR7 blocker — "who pays for inference, BYO-key vs Waggle-metered"). Unresolved; founder decision.
|
||||
3. **Checkout UI: hosted Stripe Checkout (matches current wiring) vs. inline Stripe Elements card form (matches design mock, larger scope)?** Recommend hosted — the backend already only supports it and the design says "where possible."
|
||||
4. **Which server validates a Clerk session if used — the local sidecar (would need `@clerk/fastify` wired into `local/`, currently only `isTeamMode` toggle) or the separate cloud server (`packages/server/src/plugins/auth.ts`, not reachable from `apps/web`)?**
|
||||
283
docs/redesign-warm-hive/pr7-recon/03-screen-auth-design.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# PR7 Recon · Screen 13 — Auth (Clerk, themed) · DESIGN spec
|
||||
|
||||
> RECON ONLY. No product code touched. Every claim cites `file:line`.
|
||||
> Scope: faithful component-level breakdown of the Auth screen design so a build
|
||||
> agent can implement it against the warm token system, PLUS the REAL-vs-BUILD
|
||||
> reality of auth in this monorepo (which is the load-bearing surprise here).
|
||||
|
||||
Sources read in full:
|
||||
- `docs/design_handoff_waggle_app/design-files/screens/auth.html` (181 lines)
|
||||
- `docs/design_handoff_waggle_app/SCREENS.md` §13 (lines 269-278)
|
||||
- `docs/design_handoff_waggle_app/design-files/styles/waggle.css` (155 lines, token grounding)
|
||||
- `docs/redesign-warm-hive/BUILD-PLAN.md` §6 (PR7 row) + §7.5 (the BYO/metered gate)
|
||||
- Codebase auth reality: `packages/server/src/plugins/auth.ts`, `…/local/security-middleware.ts`,
|
||||
`…/services/user-service.ts`, `apps/web/src/components/os/AppShell.tsx`,
|
||||
`apps/web/src/lib/adapter.ts`, `apps/www/app/sign-in/[[...sign-in]]/page.tsx`,
|
||||
`apps/www/app/api/stripe/checkout/route.ts`
|
||||
|
||||
---
|
||||
|
||||
## 0. The headline (read this before building)
|
||||
|
||||
**The desktop app (`apps/web`) — PR7's build target — has NO authenticated user
|
||||
identity and NO Clerk React SDK today.** Auth screen 13 is therefore overwhelmingly
|
||||
**EXTERNAL-DEP + decision-gated**, not a re-skin of something already wired.
|
||||
|
||||
Two distinct server modes coexist; the design's "Clerk" assumption only matches ONE
|
||||
of them, and it's NOT the one the desktop talks to:
|
||||
|
||||
| Mode | Auth mechanism | Has a real user account? | Where the design's screen would live |
|
||||
|---|---|---|---|
|
||||
| **Local sidecar** (what the Tauri desktop / `apps/web` talks to) | per-process **machine bearer token** via `GET /api/auth/session-token`, exchanged so loopback callers can't drive the API. NOT a login. (`packages/server/src/local/security-middleware.ts:235-377`, `:238`) | **No.** "Identity" is the local IdentityLayer name the user types in onboarding (`adapter.getIdentity()` → `/api/identity`, `IdentityResponse.name`), a memory record, not an account. (`apps/web/src/lib/adapter.ts:1090-1095`; `tauri-bindings.ts:133-145`) | n/a today — there is no `/auth` route in `apps/web` (grep for `'/auth'`/`appId.*auth` → **No matches**) |
|
||||
| **Cloud / Team server** (`packages/server/src/index.ts` + `plugins/auth.ts`) | **real Clerk** — `verifyToken()`, `clerkClient.users.getUser()`, Drizzle `users` table, auto-provision on first auth (`packages/server/src/plugins/auth.ts:3,19-52`; `services/user-service.ts:30-58`) | Yes (Clerk user → internal UUID) | n/a in `apps/web` either |
|
||||
| **`apps/www`** (Next.js landing) | **real Clerk UI**, hosted `<SignIn/>`/`<SignUp/>` catch-all pages, themed via `<ClerkProvider>` (`apps/www/app/sign-in/[[...sign-in]]/page.tsx:1,17-23`); `@clerk/nextjs@^7.3.0` + `@clerk/themes@^2.4.57` (`apps/www/package.json:15-16`) | Yes | **This is the only place Clerk's themeable React UI already exists.** |
|
||||
|
||||
So the build decision PR7 must surface: **does screen 13 ship as a real auth flow in
|
||||
the desktop at all, or is desktop auth always optional/local and "sign in for sync"
|
||||
links out to the `apps/www` Clerk flow?** The design copy itself ("account is optional —
|
||||
Waggle runs fully local without one") leans toward the latter. See §6 Decisions.
|
||||
|
||||
---
|
||||
|
||||
## 1. Exact layout (split: brand-panel left + form right)
|
||||
|
||||
From `auth.html`:
|
||||
|
||||
- **Top control bar** (`.controls`, `auth.html:78-87`) — concept-harness chrome: a label
|
||||
`Auth · Clerk · state` (`:79`), a 4-way segmented state switcher
|
||||
`Sign in / Sign up / Verify / SSO` (`:80-85`), and a theme toggle button (`:86`).
|
||||
**This bar is concept scaffolding for previewing states — NOT product UI.** In the
|
||||
real build the "state" is route/Clerk-flow-driven, not a manual segmented control.
|
||||
- **Split grid** (`.split`, `:89`; CSS `:20`) — `grid-template-columns: 1.05fr 1fr`
|
||||
(brand panel slightly wider than the form).
|
||||
- **Left brand panel** (`.brandside`, `:90-101`; CSS `:22-33`):
|
||||
- 56px padding, `linear-gradient(160deg, var(--bg-2), var(--bg))`, right border
|
||||
`--line-soft`, full-bleed honeycomb texture `.comb` masked by a radial gradient
|
||||
at 30%/30% (CSS `:23`).
|
||||
- Three vertical zones via `justify-content:space-between`: **brand lockup** (hex "W"
|
||||
mark + "Waggle" wordmark, `:92`), **pitch** (h2 + p, `:93-96`), **trust lines**
|
||||
(`:97-100`).
|
||||
- **Hidden below 820px** — `@media (max-width:820px){ .brandside{display:none} }`
|
||||
(CSS `:73`). Mobile = form only.
|
||||
- **Right form panel** (`.formside`, `:103-152`; CSS `:36-37`): centered, `max-width:380px`
|
||||
card, scrollable. Holds the four state views (`.view`, only one `.on` at a time, CSS `:72`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Every state — verbatim copy + behavior
|
||||
|
||||
### 2a. Sign in (`data-view="signin"`, `auth.html:106-117`) — default
|
||||
- Heading **"Welcome back"**; sub **"New to Waggle? Create an account"** (link → signup) (`:107`).
|
||||
- **SSO block** (`.sso`, `:108-111`): two buttons — **"Continue with Google"** (mono "G"
|
||||
badge) and **"Continue with Apple"** (mono "⌥" badge).
|
||||
- Divider **"or"** (`.divider`, `:112`).
|
||||
- **Email** field, demo value `mara@egzakta.com` (`:113`).
|
||||
- **Password** field with a `lrow` header: label + **"Forgot?"** link (→ verify view in
|
||||
the demo; in product → Clerk reset) (`:114`).
|
||||
- Primary **"Sign in"** button (`data-go="home"` → routes to Home) (`:115`).
|
||||
- Fineprint: **"By continuing you agree to the Terms & Privacy Policy."** (`:116`).
|
||||
|
||||
### 2b. Sign up (`data-view="signup"`, `:120-132`) — carries the local-first note
|
||||
- Heading **"Create your hive"**; sub **"Already have an account? Sign in"** (`:121`).
|
||||
- **`.localnote` honey banner** (`:122`; CSS `:67-70`) — THE load-bearing trust copy:
|
||||
> **"You don't need this to start."** Waggle works locally right away — create an
|
||||
> account only when you want sync or a team.
|
||||
- **SSO block**: single **"Sign up with Google"** (`:124`).
|
||||
- Divider **"or"** (`:126`).
|
||||
- Fields: **Name** (demo `Mara Kovač`), **Email** (placeholder `you@company.com`),
|
||||
**Password** (placeholder **"At least 10 characters"**) (`:127-129`).
|
||||
- Primary **"Create account"** (→ verify) (`:130`).
|
||||
- Fineprint: **"We'll send a code to verify your email."** (`:131`).
|
||||
|
||||
> Password rule "At least 10 characters" (`:129`) is design copy. Real minimum is
|
||||
> Clerk-policy-driven — do NOT hardcode "10" in validation; mirror whatever the Clerk
|
||||
> instance enforces, or omit the count.
|
||||
|
||||
### 2c. Verify — 6-box OTP (`data-view="verify"`, `:135-140`)
|
||||
- Heading **"Check your email"**; sub **"We sent a 6-digit code to mara@egzakta.com"**
|
||||
(the email is bolded in `--text-2`) (`:136`).
|
||||
- `.otp` row of **6 single-char inputs** (`:137`; CSS `:60-64`): 48×56px, mono 22px,
|
||||
`maxlength=1`, `inputmode="numeric"`; a `.filled` class flips border + text to honey
|
||||
on a non-empty box.
|
||||
- **Auto-advance / backspace nav** (the explicit design requirement), in the demo script
|
||||
(`:163-169`): `input` event focuses the next box when filled; `keydown` Backspace on an
|
||||
empty box focuses the previous box. The demo pre-fills boxes 0-2 with `[2,4,9]`.
|
||||
- Primary **"Verify & continue"** (→ Home) (`:138`).
|
||||
- **"Didn't get it? Resend code · Use a different email"** (→ back to sign in) (`:139`).
|
||||
|
||||
### 2d. SSO / enterprise (`data-view="sso"`, `:143-150`)
|
||||
- Heading **"Single sign-on"**; sub **"Use your organization's identity provider."** (`:144`).
|
||||
- Field **"Work email or organization"** (placeholder `you@company.com`) (`:145`).
|
||||
- Primary **"Continue with SSO"** (`:146`).
|
||||
- Divider **"enterprise"** (`:147`).
|
||||
- **Muted `.localnote`** (neutral, not honey — `background:var(--bg-2)`) (`:148`), verbatim:
|
||||
> SAML, SCIM provisioning, and audit logs are available on **Teams** and **KVARK**.
|
||||
> **Talk to sales →**
|
||||
- Back link **"← Back to sign in"** (`:149`).
|
||||
|
||||
### 2e. Brand-panel pitch + trust copy (verbatim, `:93-100`)
|
||||
- h2: **"Your work follows you, *everywhere.*"** ("everywhere." in honey via `em`, CSS `:29`).
|
||||
- p: **"Sign in to sync your hive across devices, collaborate with a team, and pick up any
|
||||
project exactly where you left off — on any machine."**
|
||||
- Trust line 1 (shield icon): **"An account is optional — Waggle runs fully local without one"**
|
||||
- Trust line 2 (arrow icon): **"Your memory stays yours; sign-in only adds sync"**
|
||||
|
||||
---
|
||||
|
||||
## 3. Warm tokens + primitives used (for faithful build)
|
||||
|
||||
All from `waggle.css` (dark `:9-61`, light `:63-101`). The auth HTML uses these named
|
||||
tokens directly:
|
||||
|
||||
| Primitive | Tokens (from `auth.html` `<style>` + `waggle.css`) |
|
||||
|---|---|
|
||||
| Brand-panel bg | `linear-gradient(160deg, --bg-2, --bg)`; border `--line-soft` (`auth.html:22`) |
|
||||
| Honeycomb texture | `.comb` data-URI honey @ 5% stroke, radial mask (CSS `:119-122`) |
|
||||
| Hex mark | `.hex` clip-path (`waggle.css:117`) + `linear-gradient(150deg,--honey-bright,--honey-deep)` + `--honey-glow` (`auth.html:25`) |
|
||||
| SSO buttons | `--surface` bg, `--line-strong` border; hover → `--honey-line` + `--surface-2` (`auth.html:44-45`) |
|
||||
| Divider | flex rule, `--line-soft` lines, mono `--text-dim` label (`auth.html:47-48`) |
|
||||
| Inputs | `--surface` bg, `--line` border, `--r:11px`; focus → `--honey-line` + `--honey-glow` (`auth.html:52-53`) |
|
||||
| OTP boxes | mono, `--r:12px`; `.filled` → `--honey-line` + honey text (`auth.html:62-64`) |
|
||||
| Primary submit | `--honey` bg, text `#1a1407`; hover → `--honey-bright` (`auth.html:56-57`) |
|
||||
| Honey trust banner | `.localnote` → `--honey-wash` bg + `--honey-line` border (`auth.html:68`) |
|
||||
| Neutral enterprise banner | `.localnote` overridden to `--bg-2` + `--line-soft` (`auth.html:148`) |
|
||||
| Links / accents | `--honey`; fineprint `--text-dim` (`auth.html:41,55,58`) |
|
||||
| Focus ring (global) | `:focus-visible{outline:2px solid --honey}` (`waggle.css:125`) |
|
||||
| Fonts | `--sans` Hanken Grotesk, `--mono` JetBrains Mono (`waggle.css:51-53`) |
|
||||
|
||||
In the real build these map to the **PR1-landed warm tokens** in `apps/web`
|
||||
(`BUILD-PLAN.md §3.1`, already shipped per MEMORY.md — `index.css` carries the verbatim
|
||||
`waggle.css` names + the shadcn HSL recolor). So **no new tokens are needed** — the build
|
||||
re-skins Clerk/custom components against the already-present token set. Honey "#1a1407"
|
||||
button-foreground is the same `--primary-foreground` PR1 set (`BUILD-PLAN.md:67`).
|
||||
|
||||
`#1a1407` (honey-button text) and `data-theme` theming are app-global; the screen
|
||||
inherits dark default + the warm-paper light variant for free.
|
||||
|
||||
---
|
||||
|
||||
## 4. Clerk's themeable components vs custom
|
||||
|
||||
Per SCREENS.md §13: **"Build with Clerk components themed to the tokens."** Mapping the
|
||||
design's pieces to what Clerk's `appearance` API covers:
|
||||
|
||||
| Design piece | Clerk coverage | Notes |
|
||||
|---|---|---|
|
||||
| Sign in (Google/Apple SSO + email/pw) | **`<SignIn/>`** | Social buttons, email/pw, "Forgot?" reset are first-class. Theme via `appearance.variables` (`colorPrimary` ← `--honey`, `colorBackground` ← `--surface`, etc.) + `elements` overrides. `@clerk/themes` already a www dep (`apps/www/package.json:16`). |
|
||||
| Sign up + local-first note | **`<SignUp/>`** + **custom** | The form is Clerk; the **honey `.localnote` "you don't need this to start"** banner is custom chrome placed above/around `<SignUp/>`. |
|
||||
| Verify 6-box OTP (auto-advance/backspace) | **Clerk built-in** | Clerk's email-code step renders its own OTP input with auto-advance. Re-skinning to the exact 48×56 honey boxes needs `elements.otpCodeField*` overrides (or Clerk Elements / a fully custom flow if pixel-parity is required). |
|
||||
| SSO / enterprise (SAML/SCIM → Teams/KVARK) | **partial Clerk + custom** | Clerk Enterprise SSO exists but is a paid Clerk feature + per-org config. The design's panel is mostly a **custom "Talk to sales" CTA** (KVARK funnel), not a live SAML form. Safe build: custom panel, link to sales. |
|
||||
| Left brand panel + pitch + trust lines | **fully custom** | Pure layout chrome around the Clerk `<SignIn/>`/`<SignUp/>` card. |
|
||||
|
||||
**To match the warm design, two integration styles are possible:**
|
||||
1. **Themed Clerk prebuilt** (`<SignIn appearance={…}/>`) wrapped in the custom split
|
||||
layout — fastest, matches SCREENS.md's instruction, but OTP/element pixel-parity is
|
||||
limited to what `appearance.elements` exposes.
|
||||
2. **Clerk Elements / headless** (`useSignIn`, `useSignUp`) feeding the design's exact
|
||||
custom inputs/OTP/buttons — full visual control, more code, the only way to get the
|
||||
exact 48×56 honey OTP boxes + custom SSO buttons.
|
||||
|
||||
The brand panel, dividers, local-first banner, and enterprise→sales CTA are **custom in
|
||||
either case**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Honesty contract — where Auth could fabricate (MUST be gated off)
|
||||
|
||||
Auth is the single highest-risk screen for fabrication because the desktop has no real
|
||||
account. Each of these must be **real or absent — never invented**:
|
||||
|
||||
1. **A logged-in identity that isn't real.** The demo hardcodes `mara@egzakta.com` /
|
||||
`Mara Kovač` (`auth.html:113,127,136`). A build MUST NOT pre-fill or display a fake
|
||||
signed-in user. The desktop's only "identity" is the local IdentityLayer name
|
||||
(`adapter.getIdentity()`, `apps/web/src/lib/adapter.ts:1090`), which is **not** an
|
||||
authenticated account and must never be rendered as "signed in".
|
||||
2. **Fake SSO success.** Google/Apple/SSO buttons that "succeed" without a real Clerk
|
||||
(or any) provider configured are fabrication. If Clerk isn't wired in the desktop,
|
||||
these buttons must be honestly disabled / "coming soon" / route to `apps/www`, not
|
||||
fake a session. (No Clerk publishable key path exists in `apps/web` today.)
|
||||
3. **Fake OTP verification.** The demo's "Verify & continue" advances on any input
|
||||
(`auth.html:138,164`). Real verify must check a real code via Clerk; otherwise the
|
||||
verify state must not claim to have verified anything.
|
||||
4. **"Continue → Home" as a real auth boundary.** In the demo all submits just navigate
|
||||
to Home (`auth.html:171-172`). The desktop already has a **real structural auth gate**
|
||||
(the session-token `ensureReady()` contract, `adapter.authgate.test.ts`) — but that
|
||||
gates the *local sidecar*, not a user login. The Auth screen must not imply a login
|
||||
happened when only the local app opened.
|
||||
5. **SAML/SCIM as live.** The enterprise panel names SAML/SCIM/audit logs
|
||||
(`auth.html:148`). These are Teams/KVARK/Clerk-Enterprise features — the panel is a
|
||||
**sales CTA**, and must stay one unless those are genuinely provisioned. Do not render
|
||||
a SAML form that does nothing.
|
||||
|
||||
**Gate-off rule:** if Clerk is not configured for the desktop, the entire authenticated
|
||||
path (SSO, email/pw, OTP, SSO/org) should degrade to the honest local-first framing the
|
||||
design itself already provides ("an account is optional — Waggle runs fully local") and
|
||||
a single "Sign in for sync →" link to the real flow, rather than a non-functional
|
||||
look-alike.
|
||||
|
||||
---
|
||||
|
||||
## 6. Decisions a build agent must get answered first
|
||||
|
||||
1. **Does desktop auth ship at all, or link out?**
|
||||
- Options: (a) full Clerk in `apps/web` (add `@clerk/clerk-react` + publishable key +
|
||||
a `/auth` route — none exist today); (b) desktop stays local-only, "Sign in for sync"
|
||||
deep-links to the existing `apps/www` Clerk flow (`apps/www/app/sign-in/...`); (c)
|
||||
embed/redirect to `apps/www` in a webview.
|
||||
- Recommendation: **(b)** for first ship — matches "account is optional", reuses the
|
||||
real, already-themed `apps/www` Clerk surface, and avoids standing up a second Clerk
|
||||
React integration + token bridge into the local sidecar (which currently authenticates
|
||||
with a *machine* token, not a *user* token).
|
||||
- Blast radius: large if (a) — new dep, new route, new token-exchange between Clerk
|
||||
user-JWT and the local bearer; small if (b)/(c).
|
||||
|
||||
2. **BYO-key vs Waggle-metered (DESIGN_POV §4 / BUILD-PLAN §7.5 #5).** Explicitly flagged
|
||||
as **blocking Billing/PR7** (`DESIGN_POV.md:62-70`, `:88-90`; `BUILD-PLAN.md:165-166`).
|
||||
It reshapes whether "sign in" is even required to use models (BYO = local key, no
|
||||
account needed; metered = account + payment up front). **Settle before building 13/14.**
|
||||
- Blast radius: shapes Auth (is sign-in required for inference?), Onboarding model gate,
|
||||
Billing, Usage. Founder decision, not a build choice.
|
||||
|
||||
3. **OTP fidelity: themed Clerk prebuilt vs Clerk Elements/headless.** Pixel-exact 48×56
|
||||
honey OTP boxes need headless; "good enough" needs only `appearance` overrides.
|
||||
- Recommendation: themed prebuilt first (ships SCREENS.md's instruction), upgrade to
|
||||
Elements only if review demands the exact boxes. Blast radius: small/local.
|
||||
|
||||
4. **SSO/enterprise panel = sales CTA only (no live SAML).** Recommendation: keep it a
|
||||
custom "Talk to sales → KVARK/Teams" panel; do not implement live SAML in PR7.
|
||||
Blast radius: small.
|
||||
|
||||
---
|
||||
|
||||
## 7. REAL vs DERIVABLE vs MUST-BUILD vs EXTERNAL-DEP (screen 13)
|
||||
|
||||
| Feature | Status | Evidence / note |
|
||||
|---|---|---|
|
||||
| Warm tokens + primitives the screen needs | **REAL** | PR1 landed verbatim `waggle.css` tokens in `apps/web` (`BUILD-PLAN.md §3.1`, MEMORY.md PR1). No new tokens. |
|
||||
| Split brand panel, pitch, trust lines, dividers, local-first banner, enterprise CTA | **MUST-BUILD** (custom chrome, low risk) | Pure layout/copy; no backend. All copy verbatim in §2/§4 above. |
|
||||
| Clerk `<SignIn/>`/`<SignUp/>`/OTP UI in `apps/web` | **EXTERNAL-DEP** | No `@clerk/clerk-react` in `apps/web`; no publishable key; no `/auth` route (grep: no matches). Clerk React UI exists ONLY in `apps/www` (`apps/www/app/sign-in/[[...sign-in]]/page.tsx`). |
|
||||
| Real user account / login / session | **EXTERNAL-DEP** | Real Clerk auth lives in cloud/team server (`packages/server/src/plugins/auth.ts:3,31-48`) + `users` table (`services/user-service.ts`). Desktop sidecar auth is a **machine bearer token**, not a user (`local/security-middleware.ts:235-377`). |
|
||||
| Local "identity" (name) for the user row | **REAL but NOT an account** | `adapter.getIdentity()` → `/api/identity` → IdentityLayer name (`adapter.ts:1090`; `tauri-bindings.ts:133-145`); used in `AppShell.tsx:98-108`, degrades to "Account". Must NOT be shown as "signed in". |
|
||||
| SSO with Google/Apple | **EXTERNAL-DEP** | Clerk social providers; need Clerk + OAuth app config. Not wired in desktop. |
|
||||
| SAML / SCIM (enterprise) | **EXTERNAL-DEP** (Teams/KVARK/Clerk-Enterprise) | Design panel is a sales CTA, not a live form (`auth.html:148`). |
|
||||
| "Continue → Home" navigation | **DERIVABLE** | Routes to `/home`; desktop already has the structural sidecar auth gate (`adapter.authgate.test.ts`) but that is not a user login. |
|
||||
| Theme toggle / segmented state switcher (top bar) | **N/A — concept scaffolding** | `.controls` is harness chrome for previewing states (`auth.html:78-87`), not product UI. |
|
||||
|
||||
---
|
||||
|
||||
## 8. One-paragraph build brief (for the implementer)
|
||||
|
||||
Build screen 13 as a **custom warm split layout** (left brand panel: hex "W" + honeycomb
|
||||
`.comb` + verbatim pitch/trust copy from §2e; right: a centered `max-width:380px` card)
|
||||
in `apps/web`, against the **already-present PR1 warm tokens** (no new tokens). The form
|
||||
itself is **EXTERNAL-DEP on Clerk**, which is wired only in `apps/www` today — so the
|
||||
**first, honest ship is local-first**: the desktop stays usable without an account
|
||||
(reuse the design's own "you don't need this to start" `.localnote`), and a single
|
||||
**"Sign in for sync →"** links to the real, already-themed `apps/www` Clerk flow rather
|
||||
than a non-functional Clerk look-alike in the desktop. If founder confirms full in-app
|
||||
Clerk (decision §6.1a), add `@clerk/clerk-react` + a publishable key + a `/auth` route +
|
||||
a user-JWT→local-sidecar token bridge, and theme `<SignIn/>`/`<SignUp/>`/OTP via
|
||||
`appearance` (Elements only if pixel-exact OTP boxes are required). **Never** render a
|
||||
fabricated signed-in identity, fake SSO/OTP success, or a dead SAML form (§5). The
|
||||
**BYO-vs-metered decision (§6.2) blocks this screen and Billing** and must be settled
|
||||
first.
|
||||
271
docs/redesign-warm-hive/pr7-recon/04-screen-billing-design.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# PR7 Recon — Screen 14: Billing (Stripe) DESIGN spec
|
||||
|
||||
> RECON ONLY. No product code touched. Topic owner: the Billing screen DESIGN spec
|
||||
> (`billing.html`) + SCREENS.md §14, cross-checked against the REAL Stripe wiring in the
|
||||
> monorepo so PR7 knows what is wired vs what must be gated/faked.
|
||||
>
|
||||
> Sources read in full:
|
||||
> - `docs/design_handoff_waggle_app/design-files/screens/billing.html` (247 lines)
|
||||
> - `docs/design_handoff_waggle_app/SCREENS.md` §14 (lines 282–295)
|
||||
> - `docs/redesign-warm-hive/BUILD-PLAN.md` (§6 roadmap, §7.5 BYO-vs-metered flag)
|
||||
> - Backend Stripe: `packages/server/src/stripe/{index,checkout,portal,webhook,sync}.ts`
|
||||
> - Tier system: `packages/shared/src/tiers.ts`
|
||||
> - Current billing UI: `apps/web/src/hooks/useBilling.ts`,
|
||||
> `apps/web/src/components/os/overlays/UpgradeModal.tsx`,
|
||||
> `apps/web/src/components/os/apps/SettingsApp.tsx` (Billing tab, ~L516–650)
|
||||
> - Tier route: `packages/server/src/local/routes/settings.ts` (`GET /api/tier`, L321–341)
|
||||
> - Identity name: `packages/server/src/local/routes/home.ts` (L258–271, IdentityLayer)
|
||||
> - Landing pricing: `apps/www/app/_components/Pricing.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR for the PR7 builder
|
||||
|
||||
The design is a **4-state segmented Stripe billing flow**: Plans, Checkout, Success, Manage
|
||||
(`billing.html:117–122`). The monorepo already has a **fully wired, real Stripe backend** for
|
||||
*Plans → checkout-redirect → sync → tier* and a **hosted Customer Portal** for *Manage*. But the
|
||||
design's Checkout, Success, and Manage states render a **custom card form, a fabricated receipt,
|
||||
a fabricated payment method, and a fabricated 3-line invoice list** — **none of which have a data
|
||||
source in this codebase, and none of which we should hand-build** (PCI + fabrication risk).
|
||||
|
||||
**The governing instruction is already in the design itself** (`SCREENS.md:294`):
|
||||
> "Use Stripe Checkout/Customer Portal where possible; theme to tokens."
|
||||
|
||||
So PR7's faithful-but-honest interpretation: **build the Plans state for real** (it maps 1:1 to the
|
||||
existing checkout route), and **treat Checkout/Success/Manage's in-app chrome as Stripe-hosted
|
||||
redirects**, not as locally-rendered card forms / invoice tables. The custom card form in the HTML
|
||||
is a **mockup of what Stripe Checkout shows** — we must not reimplement it.
|
||||
|
||||
**Blocking dependency (already flagged):** BUILD-PLAN §7 open-decision #5 — DESIGN_POV §4
|
||||
"who pays for inference (BYO-key vs Waggle-metered)" — must be decided before PR7. The Plans copy
|
||||
("you only pay for scale — no feature-count games") leans metered/scale framing; the product today
|
||||
is BYO-key (Settings model keys). This is a **copy + product-positioning fork**, not just a screen.
|
||||
|
||||
---
|
||||
|
||||
## 1. State 1 — PLANS (`billing.html:127–159`) — **REAL, ship it**
|
||||
|
||||
### Verbatim copy
|
||||
- Segmented control labels: `Plans` / `Checkout` / `Success` / `Manage` (`:118–121`); top eyebrow
|
||||
`Billing · Stripe · state` (`:116`).
|
||||
- Header H1: **"Upgrade your _hive._"** (`:130`).
|
||||
- Subhead: **"Memory is free forever. You only pay for scale — no feature-count games."** (`:131`).
|
||||
- Cycle toggle: **`Monthly`** | **`Annual −20%`** (`:132`); the `−20%` is a `.save` span in
|
||||
`--healthy` green.
|
||||
|
||||
### The 3 cards (verbatim)
|
||||
| Card | Tag | Price (mo) | Price (yr) | Unit suffix | Tagline | Feature list | CTA |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **Solo** | `Current` (work-blue `.curtag`, `:136`) | **$0** | $0 | `/ forever` | "For individuals exploring an AI workspace." | Personal memory graph · All major LLMs + local · Local-first by default | **"Your plan"** (disabled, `:141`) |
|
||||
| **Pro** | **"Most popular"** (honey `.pop`, `:144`) `.feat` honey border+glow | **$19** | **$15** | `/ month` → `/ mo · billed yearly` | "For power users compounding across projects." | Everything in Solo · Sync across devices · Marketplace skills & connectors · Self-evolving skills | **"Choose Pro"** (honey, `data-go="checkout"`) |
|
||||
| **Teams** | — | **$49** | **$39** | `/ seat / mo` → `/ seat · yearly` | "Shared memory without losing privacy." | Everything in Pro · Shared team memory · WaggleDance multi-agent · SSO & role-based access | **"Choose Teams"** (ghost) |
|
||||
|
||||
- Annual prices come from `data-yr` attributes; the cycle toggle JS swaps `pp` innerHTML and special-cases Teams' suffix (`:243`). So **$15 Pro / $39 Teams annual are the design's stated annual-equivalent monthly numbers** (−20% of $19→$15.20 rounded to $15; −20% of $49→$39.20 rounded to $39).
|
||||
- "Most popular" badge is **honey** (`--honey` bg, `#1a1407` text, `:38`); "Current" badge is **work-blue** (`--work`, `:39`).
|
||||
|
||||
### REAL backing
|
||||
- **Prices match `tiers.ts:7–12` exactly**: FREE $0 / PRO $19/mo / TEAMS $49/seat. ✅ No drift.
|
||||
- **"Choose Pro/Teams" → real route**: `adapter.createCheckoutSession('PRO'|'TEAMS')`
|
||||
(`adapter.ts:2667`) → `POST /api/stripe/create-checkout-session` (`checkout.ts:13–56`) →
|
||||
`stripe.checkout.sessions.create({mode:'subscription', allow_promotion_codes:true, ...})` →
|
||||
returns hosted `session.url`. **This already works** when `STRIPE_SECRET_KEY` + a price ID env are set.
|
||||
- **Monthly/Annual toggle is REAL-capable but currently NOT wired in-app.** The backend
|
||||
`priceIdForTier(tier, billingPeriod)` (`index.ts:91–100`) already resolves
|
||||
`STRIPE_PRICE_PRO_MONTHLY/_ANNUAL` + `STRIPE_PRICE_TEAMS_MONTHLY/_ANNUAL`. But
|
||||
`adapter.createCheckoutSession(tier)` (`adapter.ts:2667`) sends **no `billingPeriod`** → always
|
||||
monthly. **MUST-BUILD (small):** thread `billingPeriod` through the adapter + hook to honor the
|
||||
toggle. `apps/www/app/_components/Pricing.tsx:9,95,165` already has the monthly/annual toggle pattern to copy.
|
||||
- "Solo = Current / Your plan (disabled)" — the current-plan marker is REAL: `useBilling().tier`
|
||||
+ `tierResolved` (`useBilling.ts:13–37`); SettingsApp already renders a tier badge from this
|
||||
(`SettingsApp.tsx:537–561`).
|
||||
|
||||
**Verdict: Plans is REAL/DERIVABLE — the highest-value, lowest-risk part of PR7.**
|
||||
|
||||
---
|
||||
|
||||
## 2. State 2 — CHECKOUT (`billing.html:161–185`) — **DO NOT hand-build the card form**
|
||||
|
||||
### Verbatim copy (left "Payment details" panel, `:163–175`)
|
||||
- H2 **"Payment details"**.
|
||||
- Email field, value `mara@egzakta.com` (**fabricated identity**).
|
||||
- "Card information" → `1234 1234 1234 1234` placeholder, value `4242 4242 4242 4242` (**fake Stripe test card**), `VISA` brand chip.
|
||||
- Expiry `MM / YY` value `08 / 28`; CVC value `•••`.
|
||||
- "Name on card" value `Mara Kovač` (**fabricated**).
|
||||
- "Country" value `Germany` (**fabricated**).
|
||||
- Secure line (`:172`): lock icon + **"Encrypted & secure. We never store your card — Stripe does."**
|
||||
- Pay button (`:173`): **"Pay $19.00 / month"** → `data-go="success"`.
|
||||
- Footer (`:174`): **"Powered by _Stripe_ · cancel anytime"**.
|
||||
|
||||
### Verbatim copy (right "Order summary" panel, `:176–184`)
|
||||
- H2 **"Order summary"**.
|
||||
- Plan row: hex "W" mark + **"Waggle Pro"** / **"Monthly · renews Jul 14"** (`:178`).
|
||||
- Line: **"Pro plan" — "$19.00"** (`:179`).
|
||||
- Promo row: input placeholder **"Promo code"** + **"Apply"** button (`:180`).
|
||||
- Line: **"Tax (est.)" — "$0.00"** (`:181`).
|
||||
- Total line: **"Due today" — "$19.00"** (`:182`).
|
||||
- Guarantee (`:183`): **"14-day free trial · you won't be charged until Jun 28"**.
|
||||
|
||||
### Reality check — THIS IS THE CORE TENSION
|
||||
- **There is NO custom-card-form backend, and there must not be one.** The real flow is a
|
||||
**redirect to Stripe-hosted Checkout**: `checkout.ts` returns `session.url`, and
|
||||
`useBilling.startCheckout()` does `window.open(url, '_blank')` (`useBilling.ts:69–81`). The card
|
||||
fields, brand detection, promo `Apply`, and live tax are **all Stripe's hosted page**, not ours.
|
||||
- **`SCREENS.md:294` explicitly says "Use Stripe Checkout/Customer Portal where possible."** So the
|
||||
HTML's 2-col card form is a **visual mock of Stripe Checkout** — the honest PR7 build is:
|
||||
*"Choose Pro" → spinner/redirect → Stripe-hosted Checkout (themeable via Stripe's Branding
|
||||
settings, NOT our DOM).* We do **not** collect card/email/name/country in-app (PCI scope + the
|
||||
fields have no API to POST to).
|
||||
- **Promo codes ARE real** end-to-end: `allow_promotion_codes:true` (`checkout.ts:44`) — but they're
|
||||
entered on Stripe's page, not our `Apply` button.
|
||||
- **Trial-aware "Due today / won't be charged until"** is DERIVABLE from `trialDaysRemaining` +
|
||||
`trialStartedAt` (`/api/tier`, `settings.ts:332–333`) for a *plans-page hint*, but the binding
|
||||
"Due today $19 / charged Jun 28" on the checkout page itself is **Stripe-rendered** (Stripe knows
|
||||
the actual trial config on the price). A hardcoded "Jun 28" / "Jul 14" in our UI would be fabrication.
|
||||
|
||||
**Verdict: MUST-NOT-HAND-BUILD. Replace the custom card panel with the existing redirect-to-Stripe
|
||||
flow. The order-summary panel can be a real pre-checkout summary (plan + price from `tiers.ts`),
|
||||
but any date/tax/"due today" line must come from Stripe or be omitted — never invented.**
|
||||
|
||||
---
|
||||
|
||||
## 3. State 3 — SUCCESS (`billing.html:187–199`) — **receipt = FABRICATED, gate it**
|
||||
|
||||
### Verbatim copy
|
||||
- Healthy-green check **ring** (`.ring`, `--healthy-wash` bg, `:189`).
|
||||
- H1 **"You're _Pro._"** (`:190`).
|
||||
- Body (`:191`): **"Your hive just leveled up — _sync, the marketplace, and self-evolving skills_
|
||||
are live. Your trial runs 14 days; we'll remind you before the first charge."**
|
||||
- Receipt card (`:192–197`):
|
||||
- **"Plan" — "Waggle Pro · Monthly"**
|
||||
- **"Trial ends" — "Jun 28, 2026"** (**fabricated date**)
|
||||
- **"Then" — "$19.00 / month"**
|
||||
- **"Receipt" — "Emailed →"** (honey, clickable; **no real email-receipt feature in-app**)
|
||||
- CTA (`:198`): **"Start using Pro →"** (`data-go="home"`) + **"Manage billing"** (`data-go="manage"`).
|
||||
|
||||
### Reality check
|
||||
- **The success *trigger* is REAL**: after Stripe redirect, the app detects `?session_id=` and calls
|
||||
`POST /api/stripe/sync` (`useBilling.ts:104–115` → `sync.ts:18–90`), which **payment-gates**
|
||||
(`session.payment_status === 'paid' || 'no_payment_required'`, `sync.ts:46`) and updates the tier.
|
||||
There IS a `/payment-success?session_id=…` success_url already (`checkout.ts:42`). So a real
|
||||
"You're Pro" confirmation **can** render off the synced tier.
|
||||
- **The receipt block is fabricated.** `/api/stripe/sync` returns only `{ tier, customerId }`
|
||||
(`sync.ts:83`). **No "trial ends" date, no "$/mo then" line, no receipt number/email** is returned.
|
||||
"Trial ends Jun 28" and the "Emailed →" receipt link have **no data source** — rendering them as
|
||||
shown would invent facts. ("Receipt emailed" is even arguably true *only* if Stripe email receipts
|
||||
are enabled on the account — out of our control.)
|
||||
|
||||
**Verdict: Build the success state from the SYNCED TIER only ("You're Pro" + CTA buttons).
|
||||
GATE OFF the receipt rows (Plan/Trial-ends/Then/Receipt) unless sourced from Stripe — they are
|
||||
fabrication risks. Trial copy must be driven by real `trialDaysRemaining`, not a hardcoded "14 days
|
||||
/ Jun 28."**
|
||||
|
||||
---
|
||||
|
||||
## 4. State 4 — MANAGE (`billing.html:201–218`) — **invoice list + payment method = FABRICATED; use Customer Portal**
|
||||
|
||||
### Verbatim copy
|
||||
- H1 **"Billing"** (`:203`).
|
||||
- Current-plan card (`:204–209`, honey border):
|
||||
- **"Waggle Pro"** / **"$19.00 / month · renews Jul 14, 2026"** + **"● Active"** badge (honey).
|
||||
- Row **"Billing cycle" — "Monthly _Switch to annual (−20%)_"** (the `.chg` link is honey).
|
||||
- Row **"Payment method" — "VISA ···· 4242" _Update_** (`:207`).
|
||||
- Row **"Next charge" — "$19.00 on Jul 14"** (`:208`).
|
||||
- Invoices card (`:210–215`):
|
||||
- Header **"Invoices"**.
|
||||
- 3 rows, each: date · `$19.00` (mono) · **"Paid"** (healthy pill) · **"PDF ↓"** download link:
|
||||
- **Jun 14, 2026** · $19.00 · Paid · PDF
|
||||
- **May 14, 2026** · $19.00 · Paid · PDF
|
||||
- **Apr 14, 2026** · $19.00 · Paid · PDF
|
||||
- Footer actions (`:216`): **"Change plan"** (ghost, `data-go="plans"`) + **"Cancel subscription"** (danger/red).
|
||||
- Footer note (`:217`): **"Subscription managed securely via _Stripe_."**
|
||||
|
||||
### Reality check
|
||||
- **`renews Jul 14`, `VISA ···· 4242`, `Next charge $19.00 on Jul 14`, and ALL 3 invoices are
|
||||
fabricated.** Grep confirms **no invoices route, no payment-method route, no `invoices.list` /
|
||||
`customers.retrieve` / `paymentMethods` call anywhere in `packages/server/src`.** `/api/stripe/sync`
|
||||
+ `/api/tier` return **no renewal date, no card brand/last4, no next-charge, no invoice history.**
|
||||
- **What IS real for Manage:** the **Stripe Customer Portal**. `createPortalSession()`
|
||||
(`adapter.ts:2675`) → `POST /api/stripe/create-portal-session` (`portal.ts:14–51`, `requireTier('PRO')`,
|
||||
reads `stripe_customer_id` from config.json) → returns hosted `billingPortal` URL. **The portal IS
|
||||
where "Update payment method, view invoices, cancel subscription" actually happens** — and the
|
||||
current SettingsApp already says exactly that (`SettingsApp.tsx:632`: *"Update payment method, view
|
||||
invoices, or cancel your subscription via the Stripe customer portal."*).
|
||||
- **"Switch to annual (−20%)" / "Change plan" / "Cancel subscription"** → all **belong in the
|
||||
Customer Portal** (or a fresh checkout for an upgrade). Building in-app cancel/swap buttons that
|
||||
hit Stripe write-APIs directly is out of scope and risky; the portal is the sanctioned surface.
|
||||
|
||||
**Verdict: The Manage state's in-app "current plan / cycle / payment method / next charge / invoice
|
||||
table" must be REPLACED by (a) a real current-plan header off `useBilling().tier` and (b) a single
|
||||
"Manage subscription via Stripe" button that opens the Customer Portal. The fabricated invoice list,
|
||||
card number, renewal/next-charge dates, and PDF links MUST be gated off — there is no data for them.**
|
||||
|
||||
---
|
||||
|
||||
## 5. The custom-card-form-vs-Stripe-Checkout tension (explicit, per task)
|
||||
|
||||
| Design HTML shows | Codebase reality | PR7 resolution |
|
||||
|---|---|---|
|
||||
| In-app 2-col card form (email/card/expiry/CVC/name/country) | No card-capture endpoint; PCI-out-of-scope by design (`checkout.ts` only mints a hosted session) | **Redirect to Stripe-hosted Checkout** (existing `startCheckout` → `window.open(session.url)`). Theme via Stripe Branding, not our DOM. |
|
||||
| In-app promo `Apply`, live `Tax (est.)`, `Due today` | `allow_promotion_codes:true` (real) but applied on Stripe's page; tax/proration is Stripe-computed | Promo/tax/due-today live on the **hosted page**. An in-app pre-summary may show plan+list price from `tiers.ts` only. |
|
||||
| In-app invoice table + PDF + payment method + cancel | No invoices/payment-method/cancel route exists | **Stripe Customer Portal** (existing `openPortal`). |
|
||||
| Success receipt (#, trial-end, "emailed") | `/sync` returns `{tier, customerId}` only | Confirm off synced tier; **gate the receipt block**. |
|
||||
|
||||
`SCREENS.md:294` ("Use Stripe Checkout/Customer Portal where possible; theme to tokens") **is the
|
||||
contract**: the HTML card form / invoice table are **fidelity mockups of Stripe's hosted surfaces**,
|
||||
not a spec to reimplement. PR7 builds the **Plans** screen + the **two redirect entry points**
|
||||
(Checkout → hosted; Manage → portal) + an **honest post-redirect success** state, all themed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Fabrication-risk register (what PR7 could silently invent — gate OFF)
|
||||
|
||||
1. **Logged-in identity** — design hardcodes `mara@egzakta.com` / `Mara Kovač`. There is **no real
|
||||
account email** in any contract. The only display name available is `IdentityLayer.get().name`
|
||||
(memory-derived, often undefined — `home.ts:261–267`), surfaced as `HomeBriefing.userName`. **An
|
||||
email or "name on card" must NOT be invented.** (Note: Auth/Clerk is screen 13's job; until Clerk
|
||||
lands there is no authenticated email at all.)
|
||||
2. **Invoice list** (3× $19 Paid + PDF) — **no invoices route; pure fabrication.** Gate off → Customer Portal.
|
||||
3. **Payment method** (`VISA ···· 4242`) — **no payment-method route; fabrication.** Gate off → Portal.
|
||||
4. **Receipt number / "Emailed →" / "Trial ends Jun 28"** — `/sync` has none of these. Gate off.
|
||||
5. **Renewal / next-charge dates** (`renews Jul 14`, `Next charge $19.00 on Jul 14`) — no date in any
|
||||
contract. **Do not hardcode dates.** Trial dates only via real `trialDaysRemaining`/`trialStartedAt`.
|
||||
6. **`$15` / `$39` annual prices** — these are the **design's** annual numbers (−20% rounded). The
|
||||
*authoritative* charge is whatever the `STRIPE_PRICE_*_ANNUAL` price says. Display the design's
|
||||
marketing number is fine; **the actual charged amount must come from Stripe**, never asserted by us.
|
||||
7. **Test card `4242 4242 4242 4242`** — fine as a placeholder in a *mock*, but must never appear in
|
||||
the real (hosted) flow; it's Stripe's own test PAN.
|
||||
|
||||
---
|
||||
|
||||
## 7. REAL vs MUST-BUILD vs EXTERNAL-DEP (summary)
|
||||
|
||||
| Feature | Status | Evidence / note |
|
||||
|---|---|---|
|
||||
| Plans cards + prices ($0/$19/$49) | **REAL** | `tiers.ts:7–12`; rendered in `SettingsApp.tsx:543–558` + `UpgradeModal.tsx:163–165` |
|
||||
| "Choose Pro/Teams" → checkout session | **REAL** | `checkout.ts:13–56`, `adapter.ts:2667`, `useBilling.ts:69–81` |
|
||||
| Monthly/Annual toggle honored at checkout | **MUST-BUILD (small)** | backend ready (`index.ts:91–100`); adapter drops `billingPeriod` (`adapter.ts:2667`) |
|
||||
| Hosted Stripe Checkout (card form) | **REAL (redirect)** + **EXTERNAL-DEP** | needs `STRIPE_SECRET_KEY` + price-id envs (`index.ts:29`, `checkout.ts:30`) |
|
||||
| Promo code | **REAL (on hosted page)** | `allow_promotion_codes:true` (`checkout.ts:44`) |
|
||||
| Post-checkout sync → tier flip | **REAL** | `sync.ts:18–90` (payment-gated), `useBilling.ts:104–115` |
|
||||
| Success "You're Pro" off synced tier | **DERIVABLE** | from `useBilling().tier` after sync |
|
||||
| Success receipt rows (#/trial-end/then) | **MUST-GATE (fabrication)** | `/sync` returns only `{tier, customerId}` (`sync.ts:83`) |
|
||||
| Manage: current plan header | **DERIVABLE** | `useBilling().tier`, `tierResolved` |
|
||||
| Manage: payment method / invoices / cancel / next-charge | **MUST-GATE → Customer Portal** | no route exists; `portal.ts:14–51` is the sanctioned surface; SettingsApp already does this (`SettingsApp.tsx:622–634`) |
|
||||
| Stripe Customer Portal | **REAL** + **EXTERNAL-DEP** | `portal.ts`, needs `stripe_customer_id` in config.json + Stripe account |
|
||||
| Authenticated user email/name | **EXTERNAL-DEP (Clerk, screen 13)** | no account email in any contract; `userName` is memory-derived only (`home.ts:261`) |
|
||||
| BYO-key vs metered positioning ("pay for scale" copy) | **EXTERNAL-DEP (founder decision)** | BUILD-PLAN §7 #5 / DESIGN_POV §4 — **decide before PR7** |
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommended PR7 shape for Billing (so the builder doesn't fabricate)
|
||||
|
||||
1. **Plans (real):** port the 3-card grid + monthly/annual toggle from `billing.html`; bind prices to
|
||||
`tiers.ts`; wire the toggle through a new `billingPeriod` arg on `createCheckoutSession`.
|
||||
2. **Checkout (redirect):** "Choose Pro/Teams" → spinner → `window.open(session.url)`. Optional in-app
|
||||
pre-summary with plan + list price ONLY. **No card fields, no fake tax/dates.**
|
||||
3. **Success (synced):** themed "You're Pro" off the post-`?session_id=` synced tier. **Receipt block
|
||||
omitted** (or shows only what `/sync` returns: tier). Trial line only from real trial fields.
|
||||
4. **Manage (portal):** themed current-plan header (real tier) + one "Manage subscription via Stripe"
|
||||
button → Customer Portal. **No in-app invoice table / card / cancel.**
|
||||
5. **Graceful degradation:** every state must handle Stripe-not-configured (503 `STRIPE_NOT_CONFIGURED`,
|
||||
`index.ts:5`) and unresolved tier (`tierResolved=false`, `useBilling.ts:16`) — already the SettingsApp pattern.
|
||||
251
docs/redesign-warm-hive/pr7-recon/05-clerk-integration.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# PR7 Recon · 05 — Clerk Integration (themed, reconciled with local-first)
|
||||
|
||||
> Scope: Auth screen 13 (`auth.html`) of the warm-Hive redesign. RECON ONLY — no product
|
||||
> code touched. Every claim cites `file:line` verified this session (2026-06-18).
|
||||
> Topic: how to integrate Clerk into THIS stack (React 19 + TS + **Vite SPA** `apps/web`,
|
||||
> React Router 6.30, also bundled into a Tauri desktop binary), themed to the warm tokens,
|
||||
> reconciled with "account is optional / local-first."
|
||||
|
||||
---
|
||||
|
||||
## TL;DR verdict (the highest-uncertainty recon)
|
||||
|
||||
1. **Stack reality:** `apps/web` is a **Vite SPA** (`vite@^5.4.19`, `react-router-dom@^6.30.1`,
|
||||
declarative `<BrowserRouter>`/`<Routes>` — NO loaders, NO SSR). The correct Clerk package
|
||||
is **`@clerk/clerk-react`** (skill name `@clerk/react`), env **`VITE_CLERK_PUBLISHABLE_KEY`**.
|
||||
**NOT** `@clerk/react-router` (that is React-Router-v7 *framework* mode with
|
||||
middleware+`rootAuthLoader`, which this app does not use).
|
||||
2. **Prior art exists and is excellent:** `apps/www` (the Next.js landing) already has a **full,
|
||||
themed Clerk integration** — `ClerkProvider` + `dark` baseTheme + Hive `appearance` map
|
||||
(`apps/www/app/layout.tsx:6-93,171`), hosted `/sign-in` + `/sign-up` catch-all routes,
|
||||
`/account`, `middleware.ts`. The server already verifies Clerk JWTs (`packages/server/src/plugins/auth.ts`).
|
||||
So Clerk is **REAL** in the repo — just **absent from `apps/web`** (the SPA target for screen 13).
|
||||
3. **ARCHITECTURE VERDICT (founder decision required):** **Option (b) — optional Clerk sign-in
|
||||
that unlocks sync/Teams/billing; the local desktop stays fully accountless by default.**
|
||||
This is the only option consistent with both the design copy ("An account is optional — Waggle
|
||||
runs fully local without one", `SCREENS.md:273-274`) AND the verified backend (the desktop
|
||||
sidecar is accountless: `wsSessionToken` loopback auth + `config.json` tier, `local/index.ts:2047-2052`,
|
||||
`local/routes/settings.ts:309-331`). It also matches the **prior ratified decision** that Tauri
|
||||
Clerk is a "Phase 2 fast-follow, NOT Day 0" (`2026-05-03…brief:225`).
|
||||
4. **EXTERNAL-DEP the founder must provide:** a Clerk **publishable key** for the SPA
|
||||
(`VITE_CLERK_PUBLISHABLE_KEY=pk_…`). Keys already exist for `apps/www`/server
|
||||
(`pk_test_ZWxlZ2FudC1jYW1lbC04…` in `…brief:143`; secret **rotated 2026-05-12** per
|
||||
`docs/launch/drafts/2026-05-12-apps-www-deployment-readiness.md:161`). The desktop default
|
||||
path needs **no key** (accountless).
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack verification (what `apps/web` actually is)
|
||||
|
||||
| Claim | Evidence |
|
||||
|---|---|
|
||||
| Vite SPA, not Next | `apps/web/package.json:7` `"dev":"vite"`, `:95` `"vite":"^5.4.19"`; entry `apps/web/src/main.tsx:18` `createRoot(...).render(<App/>)` |
|
||||
| React 19 | `apps/web/package.json:59` `"react":"^19.2.0"`, `:61` react-dom 19.2 |
|
||||
| React Router **6.30**, declarative | `apps/web/package.json:64` `"react-router-dom":"^6.30.1"`; `apps/web/src/App.tsx:2,60,62-102` `<BrowserRouter>`/`<Routes>` — NO `createBrowserRouter`, NO loaders |
|
||||
| shadcn/ui installed | `apps/web/components.json` exists (verified); BUILD-PLAN §2 "shadcn/ui fully installed" |
|
||||
| Tauri serves the SAME `apps/web` dist | `app/src-tauri/tauri.conf.json:7` `"frontendDist":"../../apps/web/dist"`, `:8` `devUrl http://localhost:8080` |
|
||||
| `VITE_` env prefix already used | `apps/web/.env.example:11` `VITE_POSTHOG_KEY=…` |
|
||||
| **No Clerk in `apps/web` today** | `grep '@clerk' apps/web/package.json` → none; `grep -rln 'ClerkProvider|SignIn|useSignIn' apps/web/src` → **0 files**. Clean slate for screen 13. |
|
||||
|
||||
`@clerk/react` IS present under `node_modules/@clerk/react` but only as a **transitive** dep of
|
||||
`@clerk/nextjs` (apps/www) — not a direct `apps/web` dependency. PR7 must add it explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 2. Clerk in the repo today (prior art — REAL)
|
||||
|
||||
| Surface | What exists | Evidence |
|
||||
|---|---|---|
|
||||
| `apps/www` (Next.js landing) | `@clerk/nextjs@^7.3.0` + `@clerk/themes@^2.4.57`; `ClerkProvider` in `<body>` with `baseTheme:dark` + full Hive `appearance.variables`+`elements` map | `apps/www/package.json:15-16`; `apps/www/app/layout.tsx:6-7,35-93,171-173` |
|
||||
| `apps/www` hosted auth | `<SignIn/>` at catch-all `/sign-in/[[...sign-in]]/page.tsx`, `/sign-up`, `/account` | `apps/www/app/sign-in/[[...sign-in]]/page.tsx:1,17-23` |
|
||||
| `apps/www` middleware | `clerkMiddleware()` (all routes public, per-route `auth.protect()`) | `apps/www/middleware.ts:6,16` |
|
||||
| Server JWT verify | `@clerk/fastify` `verifyToken` + `createClerkClient`; `authenticate` decorator; auto-provisions internal user from Clerk claims (`upsertFromClerk`) | `packages/server/src/plugins/auth.ts:3,21,31-48` |
|
||||
| Server config | `clerkSecretKey`/`clerkPublishableKey` from env (empty string default = solo mode) | `packages/server/src/config.ts:6-7,26-27` |
|
||||
| Env contract | `CLERK_SECRET_KEY` / `CLERK_PUBLISHABLE_KEY` documented across `.env.example:32-33`, `render.yaml:47-49`, `docker-compose.production.yml:8-9,35-36` |
|
||||
| **Team-mode gate** | Clerk-dependent server behavior activates **only when `CLERK_SECRET_KEY` is set**; absent ⇒ "solo/desktop mode" | `packages/server/src/ws/gateway.ts:46-54`; `packages/server/tests/local/session-timeout.test.ts:166-179` |
|
||||
|
||||
**Themed-Clerk pattern is already solved once** (`apps/www/layout.tsx`). PR7's SPA work is to port
|
||||
that appearance approach to `@clerk/clerk-react`, recolored to the **warm** tokens (apps/www uses the
|
||||
**old cooler** Hive hex `#08090c`/`#e5a000`; apps/web is now warm `#14110b`/`#e9a52c`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Local-first crux — why the desktop must stay accountless
|
||||
|
||||
The desktop sidecar (`packages/server/src/local/`) authenticates with a **machine-local loopback
|
||||
token**, NOT a Clerk identity:
|
||||
|
||||
- `GET /api/auth/session-token` returns `server.agentState.wsSessionToken` — "auth-exempt but
|
||||
same-origin gated… The Tauri webview reads this once on connect() and sends it as a Bearer"
|
||||
(`local/index.ts:2043-2052`). This is a **device** token, not a **user**.
|
||||
- Tier resolves from a local file, default **FREE**, with PATCH noting "will be replaced by Stripe
|
||||
webhook" (`local/routes/settings.ts:309-318,322-331,360`). There is **no logged-in user identity
|
||||
on the desktop today.**
|
||||
- The SPA adapter already injects `Authorization: Bearer <token>` on every non-exempt request
|
||||
(`apps/web/src/lib/adapter.ts:445`), bootstrapping the token in `connect()`
|
||||
(`adapter.ts:254-260,298-308`) with a 401→refresh→retry leg (`adapter.ts:311-321,465`).
|
||||
|
||||
**This is the seam Clerk plugs into.** `useAuth().getToken()` from `@clerk/clerk-react` returns the
|
||||
Clerk session JWT in exactly the `Bearer` shape the adapter + `packages/server/src/plugins/auth.ts`
|
||||
already consume — so an *optional* Clerk sign-in can swap the device token for a user JWT **only when
|
||||
the user opts into cloud/Teams**, leaving the accountless local path untouched.
|
||||
|
||||
---
|
||||
|
||||
## 4. ARCHITECTURE VERDICT — does Clerk fit "account is optional"?
|
||||
|
||||
**Recommend Option (b): optional Clerk sign-in that unlocks sync / Teams / billing; local desktop
|
||||
stays accountless by default.**
|
||||
|
||||
| Option | Fit | Why |
|
||||
|---|---|---|
|
||||
| (a) Clerk only on SaaS cloud (`apps/www`), desktop never signs in | Partial | Already true today, but screen 13 lives in `apps/web` (the SPA the desktop loads). A pure-(a) reading means screen 13 is a **cloud-only** surface and the desktop shows no auth at all — contradicts having an Auth screen in the app shell. |
|
||||
| **(b) Optional Clerk in the SPA; accountless is the default; sign-in unlocks sync/Teams/billing** | **Best** | Matches design copy ("account is optional", `SCREENS.md:273-274`), matches the accountless sidecar (`local/index.ts:2047-2052`), matches the prior "Tauri Clerk = Phase 2 fast-follow, NOT Day 0" decision (`…brief:225`), and matches the existing `useBilling` tier flow that already gates upgrade behind a server tier. Clerk renders only when `VITE_CLERK_PUBLISHABLE_KEY` is present; absent ⇒ screen 13 shows the local-first "you're running fully local" state with no fake identity. |
|
||||
| (c) Full Clerk gate (must sign in to use the app) | **Reject** | Directly violates local-first + "account is optional"; breaks the accountless desktop boot (`/api/tier` defaults FREE with no user). Do not build. |
|
||||
|
||||
**Open sub-question for the founder (genuinely unknown):** in Option (b), does desktop Clerk sign-in
|
||||
even run inside the **Tauri WebView**? Clerk's hosted OAuth/Account-Portal flow assumes a browser
|
||||
redirect; in a desktop WebView the SSO redirect (Google/Apple) may need a system-browser + deep-link
|
||||
loopback, or Clerk's custom-flow (`useSignIn`) with email OTP only. This is the same unknown the prior
|
||||
brief deferred to "Phase 2." **Recommendation:** ship screen 13 as the **web/cloud-served** surface
|
||||
first (browser context, where the apps/www pattern is proven), and treat in-WebView desktop sign-in as
|
||||
an explicit follow-up requiring a Tauri deep-link/OAuth spike. Flag, don't guess.
|
||||
|
||||
---
|
||||
|
||||
## 5. The minimal, correct THEMED integration (for Option b)
|
||||
|
||||
### 5.1 Package + env (EXTERNAL-DEP)
|
||||
- Add `@clerk/clerk-react` (current SDK, pairs with apps/www's `@clerk/nextjs` v7) + `@clerk/themes`.
|
||||
- `VITE_CLERK_PUBLISHABLE_KEY=pk_…` (founder provides; reuse the existing `apps/www` instance key).
|
||||
Vite SPA = **publishable key only**; the secret stays server-side (`CLERK_SECRET_KEY`, already wired).
|
||||
|
||||
### 5.2 ClerkProvider placement
|
||||
- Wrap `<App/>` (or just the auth-aware subtree) in `apps/web/src/main.tsx` — same level as the
|
||||
existing `createRoot(...).render(<App/>)` (`main.tsx:18`). `ClerkProvider` must sit **above**
|
||||
`react-query`/router but the design only needs auth in the screen-13 route + the sidebar user row,
|
||||
so it can wrap inside `<App/>` if a no-key fallback is desired.
|
||||
- **No-key guard (local-first):** if `import.meta.env.VITE_CLERK_PUBLISHABLE_KEY` is undefined, render
|
||||
children **without** ClerkProvider and show the accountless state — never crash, never fabricate a user.
|
||||
|
||||
### 5.3 Appearance → warm CSS tokens (the themed part)
|
||||
- **shadcn theme first.** `apps/web/components.json` exists, so per `clerk-custom-ui` the correct first
|
||||
step is `appearance={{ theme: shadcn }}` (`@clerk/themes` shadcn, current SDK). Clerk's shadcn theme
|
||||
reads the shadcn HSL vars — which PR1 already repointed to warm values:
|
||||
`--primary:38 81% 54% (#e9a52c)`, `--background:40 29% 6% (#14110b)`, `--ring:38 81% 54%`
|
||||
(`apps/web/src/index.css:20,29,46`). So most theming is **automatic**.
|
||||
- Thin override on top, mirroring apps/www's pattern but with warm hex:
|
||||
`variables:{ colorPrimary:'#e9a52c', colorBackground:'#14110b', colorText:'#…', borderRadius:'8px'
|
||||
(=--r-sm, index.css:176), fontFamily:'Hanken Grotesk, system-ui' }`.
|
||||
- Light/dark: Clerk's default theme respects CSS `color-scheme`; `apps/web/src/index.css:185` sets
|
||||
`color-scheme:dark` (+ a `[data-theme="light"]` block at `:192`). Theme stacking
|
||||
`[shadcn, dark]` or `color-scheme`-driven both work; reconcile with the existing `ThemeProvider`.
|
||||
- apps/www's `layout.tsx:31-34` carries a real gotcha to copy: **do NOT use `as const`** on the
|
||||
appearance object (over-narrows Clerk's `Appearance` union and silently drops `baseTheme`).
|
||||
|
||||
### 5.4 Prebuilt vs custom-flow components (what the design needs)
|
||||
Screen 13 (`SCREENS.md:269-278`) wants: split brand panel + form; **Sign in (Google/Apple SSO +
|
||||
email/password)**, **Sign up**, **6-box OTP Verify (auto-advance, backspace nav)**, **SSO/enterprise**.
|
||||
|
||||
| Design element | Clerk mapping | Real/Build |
|
||||
|---|---|---|
|
||||
| Sign in / Sign up form | Prebuilt `<SignIn/>` / `<SignUp/>` (themed) — cheapest, proven in apps/www | REAL component, themed = small build |
|
||||
| Google/Apple SSO | Clerk social connections (config in Clerk dashboard) — rendered by prebuilt comps automatically | EXTERNAL-DEP (OAuth creds in dashboard) |
|
||||
| Email/password | Clerk default — prebuilt | REAL |
|
||||
| **6-box OTP Verify** | This is Clerk's **email-code verification step**, which `<SignIn/>`/`<SignUp/>` render *as their own UI*. The design's bespoke 6-box auto-advance widget = **custom flow** via `useSignIn`/`useSignUp` (`clerk-custom-ui` core-3) **only if** they want the exact 6-box look; otherwise accept Clerk's built-in code input. | DERIVABLE (prebuilt) or MUST-BUILD (custom 6-box) — **founder choice** |
|
||||
| SSO/SAML/SCIM → Teams/KVARK | Clerk **Organizations/Enterprise SSO** (`clerk-orgs`) — a "note → Teams/KVARK", not a live SAML flow in PR7 | note only; Orgs are a later/Teams concern |
|
||||
| User row in sidebar (PR1 left `userName={null}`, BUILD-PLAN §9) | `<UserButton/>` (prebuilt popover) or `useUser()` to thread `HomeBriefing.userName` | DERIVABLE |
|
||||
|
||||
**Recommendation:** use **prebuilt `<SignIn/>`/`<SignUp/>` themed** for v1 (matches apps/www, lowest risk,
|
||||
"Build with Clerk components themed to the tokens" is literally the design note, `SCREENS.md:277-278`).
|
||||
Only drop to `useSignIn` custom flow if the founder insists on the pixel-exact 6-box OTP widget.
|
||||
|
||||
---
|
||||
|
||||
## 6. Billing half of PR7 (screen 14) — mostly already REAL (brief note; not my topic)
|
||||
|
||||
Flagged because PR7 bundles Auth+Billing and the honesty contract spans both:
|
||||
- `apps/web/src/hooks/useBilling.ts` **already exists** — `getTier`, `createCheckoutSession('PRO'|'TEAMS')`,
|
||||
`createPortalSession`, `syncStripeCheckout(sessionId)`, post-redirect `?session_id=` auto-sync
|
||||
(`useBilling.ts:34-115`). Adapter methods at `adapter.ts:2658-2676`.
|
||||
- Server Stripe module exists: `packages/server/src/stripe/{checkout,portal,webhook,sync,index}.ts`.
|
||||
- ⇒ Screen 14 is largely a **re-skin of the existing flow to warm tokens** + Stripe-hosted
|
||||
Checkout/Customer-Portal (open in browser). **No card form is implemented in-app** today and the
|
||||
design's "card 4242…, expiry/CVC" panel must **not** be hand-rolled — route to Stripe Checkout.
|
||||
- **Blocked decision (DESIGN_POV §4, `DESIGN_POV.md:62-70`):** BYO-key vs Waggle-metered inference.
|
||||
This reshapes Billing/Onboarding/Usage and "must be settled before Billing goes live"
|
||||
(`DESIGN_POV.md:89`). Surface to founder before building screen 14.
|
||||
|
||||
---
|
||||
|
||||
## 7. FABRICATION RISKS (must be gated off — honesty contract)
|
||||
|
||||
PR7 is the **highest fabrication-risk PR** because Auth+Billing both render identity/money:
|
||||
|
||||
1. **Fake logged-in identity.** With no `VITE_CLERK_PUBLISHABLE_KEY`, the SPA must show the
|
||||
**accountless** state, never a placeholder "signed-in" user, name, avatar, or email. The sidebar
|
||||
user row already correctly renders "Account"/"W" when `userName={null}` (BUILD-PLAN §9) — keep that
|
||||
honest; only populate from a **real** `useUser()` / `HomeBriefing.userName`.
|
||||
2. **Fake invoices / receipts.** Screen 14 "invoices (Paid + PDF)" must come from Stripe
|
||||
(Customer Portal), never a hardcoded invoice list. If no Stripe customer exists → empty/"manage in
|
||||
portal", not invented rows.
|
||||
3. **Fake payment method.** "VISA ···4242, Update" must reflect a real Stripe payment method or render
|
||||
the empty/portal state. Do NOT ship a literal `···4242` as if it were the user's card.
|
||||
4. **Fake usage / "Due today $19".** Trial-aware amounts must come from the real tier
|
||||
(`useBilling.tierResolved`, `useBilling.ts:16-22` — it explicitly forbids presenting the `FREE`
|
||||
default as fact) and Stripe price data, never a static string.
|
||||
5. **Fake card-entry form.** The design shows a card form ("encrypted & secure, Powered by Stripe").
|
||||
Collecting card data in-app is both a fabrication trap and a PCI risk — **use Stripe Checkout**,
|
||||
render the form only as Stripe's hosted/embedded element.
|
||||
6. **Fake SSO success.** SSO buttons must do a real Clerk redirect; never simulate "Signed in with
|
||||
Google" without a Clerk session.
|
||||
7. **Tier never silently FREE.** Already enforced by `useBilling.tierResolved` — keep any new auth/billing
|
||||
surface honoring it (render "unresolved", not the FREE upgrade grid, until a real round-trip).
|
||||
|
||||
---
|
||||
|
||||
## 8. What's REAL vs DERIVABLE vs MUST-BUILD vs EXTERNAL-DEP
|
||||
|
||||
| Feature | Status | Note |
|
||||
|---|---|---|
|
||||
| Clerk JS SDK + JWT model | REAL | `@clerk/fastify` server verify (`plugins/auth.ts`), `@clerk/nextjs` themed (`apps/www/layout.tsx`) |
|
||||
| Clerk in `apps/web` SPA | MUST-BUILD | add `@clerk/clerk-react` + `ClerkProvider` in `main.tsx`; **none today** |
|
||||
| Themed appearance (warm tokens) | DERIVABLE | shadcn theme auto-reads warm shadcn vars (`index.css:20,29,46`) + thin `variables` override; pattern proven in `apps/www/layout.tsx:35-93` |
|
||||
| Prebuilt `<SignIn/>`/`<SignUp/>`/`<UserButton/>` | REAL (Clerk) | design says "Build with Clerk components themed" (`SCREENS.md:277`) |
|
||||
| Bespoke 6-box OTP widget | MUST-BUILD (optional) | only if not accepting Clerk's built-in code step; `useSignIn` custom flow |
|
||||
| `getToken()` → existing Bearer adapter | DERIVABLE | adapter already sends `Authorization: Bearer` (`adapter.ts:445`); server already verifies (`plugins/auth.ts:31`) |
|
||||
| Accountless local-first default | REAL | sidecar loopback token + FREE config (`local/index.ts:2047-2052`, `settings.ts:318`) |
|
||||
| Billing flow (checkout/portal/sync/tier) | REAL | `useBilling.ts` + adapter + `server/src/stripe/*` all exist |
|
||||
| In-app card form | EXTERNAL-DEP (Stripe-hosted) | do not hand-roll; Stripe Checkout |
|
||||
| Google/Apple SSO, SAML/SCIM | EXTERNAL-DEP | OAuth creds + Clerk Orgs/Enterprise config in Clerk dashboard |
|
||||
| `VITE_CLERK_PUBLISHABLE_KEY` for SPA | EXTERNAL-DEP | **founder must provide**; instance/keys already exist for apps/www/server |
|
||||
| In-WebView desktop sign-in (Tauri) | UNKNOWN / spike | redirect/OAuth in WebView unproven; prior brief deferred to "Phase 2" (`…brief:225`) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Decisions the founder must make before PR7 builds screen 13
|
||||
|
||||
1. **Architecture:** confirm Option **(b)** — optional Clerk, accountless default. (Recommended.)
|
||||
Blast radius: defines `main.tsx` provider wrapping + the no-key fallback for the whole SPA.
|
||||
2. **Surface scope:** does screen 13 ship as a **browser/cloud-served** surface first (proven), with
|
||||
**in-WebView Tauri sign-in** as an explicit follow-up spike? (Recommended yes.)
|
||||
3. **OTP UI:** accept Clerk's built-in verification step (cheap, prebuilt) vs MUST-BUILD the pixel-exact
|
||||
6-box widget via `useSignIn`. (Recommend prebuilt for v1.)
|
||||
4. **EXTERNAL-DEP:** provide `VITE_CLERK_PUBLISHABLE_KEY` (reuse existing instance) + confirm
|
||||
Google/Apple social connections are enabled in the Clerk dashboard.
|
||||
5. **Billing prerequisite (DESIGN_POV §4):** BYO-key vs Waggle-metered — settle before screen 14.
|
||||
|
||||
---
|
||||
|
||||
## 10. Honesty log / discrepancies surfaced
|
||||
|
||||
- The task framing assumed `clerk-react-router-patterns` might apply. It does **not** — that skill is
|
||||
for React-Router **v7 framework mode** (SSR loaders + `clerkMiddleware`). This app is RR6 SPA ⇒
|
||||
`@clerk/clerk-react` (`clerk-react-patterns`) is the correct skill. Documented to prevent a wrong build.
|
||||
- apps/www's themed Clerk uses the **old cooler** Hive hex (`#08090c`/`#e5a000`,
|
||||
`apps/www/layout.tsx:38-44`). Copy the *pattern*, not the *hex* — apps/web is warm
|
||||
(`#14110b`/`#e9a52c`, `index.css:20,29`).
|
||||
- `@clerk/react` is in `node_modules` (transitive via nextjs) but **not** an apps/web dep — do not
|
||||
assume it's "already installed" for the SPA.
|
||||
- Server `authenticate`/team-mode is gated on `CLERK_SECRET_KEY` presence; the desktop default (no key)
|
||||
is the accountless path. PR7 must not assume Clerk is always on.
|
||||
320
docs/redesign-warm-hive/pr7-recon/06-routing-surfaces.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# PR7 Recon — 06 · Routing & Surfaces (Auth /auth + Billing /billing)
|
||||
|
||||
> Topic: where `/auth` and `/billing` routes + entries live, reusing the PR1–PR6 shell
|
||||
> patterns. RECON ONLY — no product code touched. Every claim cites `file:line`.
|
||||
> Verified against `origin/main @ 3764bc13` (PR1–PR6 all shipped).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (the two registration shapes)
|
||||
|
||||
1. **`/auth` (screen 13) is SPECIAL — it is the ONE pre-shell, full-screen route.** Every
|
||||
prior warm-Hive screen mounts INSIDE the `<AppShell>` layout route (`App.tsx:63-101`);
|
||||
`/auth` must NOT. It belongs as a **sibling `<Route>` at the top level, outside the
|
||||
`path="/"` AppShell element** — no Sidebar, no StatusBar, no ChatHost, no boot gate. It
|
||||
is the only screen in the whole redesign that breaks the "child-of-AppShell" rule.
|
||||
|
||||
2. **`/billing` (screen 14) is NOT a new top-level route at all.** A complete Stripe billing
|
||||
surface ALREADY EXISTS as **Settings → "Plan" tab** (`SettingsApp.tsx:511-655`), wired to
|
||||
the real `useBilling` hook → real adapter Stripe calls → real server routes. PR7's billing
|
||||
work is **(a) reskin that existing tab to the warm tokens + the SCREENS §14 4-state layout,
|
||||
and (b) make it deep-linkable** (today `/settings` always opens on the Models tab and has
|
||||
**no `?tab=` reader** — see the breadcrumb/deep-link gap below). A standalone themed
|
||||
`/billing` route is OPTIONAL and only justified if the design wants the full-screen
|
||||
Plans/Checkout/Success/Manage flow outside the Settings chrome.
|
||||
|
||||
---
|
||||
|
||||
## 1. The route table today (`apps/web/src/App.tsx`)
|
||||
|
||||
`App.tsx:52-110` — a SINGLE layout route owns everything:
|
||||
|
||||
```
|
||||
<Route path="/" element={<AppShell />}>
|
||||
<Route index element={<IndexRedirect />} />
|
||||
…all 28 child routes (home, workspaces, memory, …, benchmarks, platform)…
|
||||
<Route path="*" element={<NotFound />} /> // App.tsx:100
|
||||
</Route>
|
||||
```
|
||||
|
||||
- **Every** screen is a child of `<AppShell>` (`App.tsx:63`). There is currently **no
|
||||
top-level route outside the shell** at all.
|
||||
- Imports come from the `@/routes` barrel (`App.tsx:12-39`); the catch-all `*` must stay last
|
||||
(`App.tsx:99` comment "ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL").
|
||||
- PR6a added the two ⌘K-only static surfaces directly here: `benchmarks` (`App.tsx:97`) and
|
||||
`platform` (`App.tsx:98`).
|
||||
- **No `/auth`, `/billing`, `/login`, `/sign-in`, `/payment-success`, `/payment-cancelled`
|
||||
routes exist** (verified by grep — zero hits in `App.tsx` / `routes/index.ts`).
|
||||
|
||||
---
|
||||
|
||||
## 2. The PR6 route-wrapper pattern (to copy)
|
||||
|
||||
A PR6 ⌘K-only surface is 3 small pieces. `BenchmarkRoute.tsx:1-11` is the template:
|
||||
|
||||
```tsx
|
||||
/** PR6a route wrapper — `/benchmarks` → BenchmarkApp (static, ⌘K-only surface). */
|
||||
import BenchmarkApp from '@/components/os/apps/BenchmarkApp';
|
||||
import SurfaceBoundary from './SurfaceBoundary';
|
||||
const BenchmarkRoute = () => (
|
||||
<SurfaceBoundary appName="Benchmarks"><BenchmarkApp /></SurfaceBoundary>
|
||||
);
|
||||
export default BenchmarkRoute;
|
||||
```
|
||||
|
||||
The three pieces for any in-shell surface:
|
||||
1. **`routes/<Name>Route.tsx`** — thin wrapper that renders the App component inside
|
||||
`<SurfaceBoundary appName="…">` (`SurfaceBoundary.tsx:10-17` wraps in `AppErrorBoundary`,
|
||||
`onClose` → `navigate('/home')`).
|
||||
2. **`routes/index.ts` barrel export** (`routes/index.ts:56-59` — PR6 lines).
|
||||
3. **`<Route path="…" element={<…Route/>}/>` in `App.tsx`** under the AppShell layout
|
||||
(`App.tsx:96-98` — the PR6a block).
|
||||
|
||||
> **PR7 BILLING (if a standalone `/billing` is wanted)** follows this exact 3-step pattern
|
||||
> — a `BillingRoute` wrapper → barrel → child route under AppShell. **PR7 AUTH does NOT** —
|
||||
> see §4: it is a sibling route, not an AppShell child, and `SurfaceBoundary`'s
|
||||
> `onClose → /home` is wrong for a pre-login screen.
|
||||
|
||||
---
|
||||
|
||||
## 3. Command-catalog entries (`apps/web/src/lib/command-catalog.ts`)
|
||||
|
||||
The ⌘K catalog is built by `buildCommandCatalog()` (`command-catalog.ts:59-118`), grouped
|
||||
**Jump to / Do / Power tools** (+ Pro "★ Pinned"). Entry shape `CatalogCommand`
|
||||
(`command-catalog.ts:22-40`): `{ id, group, name, subtitle, icon, to?, action?, meta?,
|
||||
minBillingRank?, pinned? }`. Each routes to a REAL `to:` or fires `action:'spawn'`.
|
||||
|
||||
**Billing is already represented in the catalog — but points at the wrong place:**
|
||||
- `command-catalog.ts:75` — `Settings · "models · failover · permissions · plan"` → `to:"/settings"`
|
||||
- `command-catalog.ts:76` — **`"Upgrade to Pro"` · `"plans · billing · invoices"` → `to:"/settings"`**
|
||||
|
||||
Both land on `/settings` (which opens on the **Models** tab, NOT Plan — see §6 gap). PR7
|
||||
should retarget these to whatever the billing entry point becomes (`/settings?tab=billing`
|
||||
once a `?tab=` reader exists, or a new `/billing`).
|
||||
|
||||
**Auth has NO catalog entry and should NOT get one** — sign-in is a pre-login full-screen
|
||||
route reached by redirect, not a ⌘K jump from inside the authenticated shell.
|
||||
|
||||
The catalog's tier-gating mechanism to reuse: `gate()` (`command-catalog.ts:99-100`) filters
|
||||
on `minBillingRank` (FREE 0 / TRIAL 1 / PRO 2 / TEAMS 3 / ENT 4, `command-catalog.ts:36`); Pro
|
||||
"★ Pinned" floats `pinned:true` items (`command-catalog.ts:104-111`). Catalog is consumed in
|
||||
`AppShell.tsx:272-279` (`buildCommandCatalog` + `handleCatalogSelect`).
|
||||
|
||||
---
|
||||
|
||||
## 4. `/auth` — the pre-shell, full-screen registration (THE special case)
|
||||
|
||||
### Why it can't be an AppShell child
|
||||
`<AppShell>` (`AppShell.tsx:436-466`) wraps everything in `<ShellProvider>` →
|
||||
`<ShellLayout>` (`AppShell.tsx:68`), which renders the BootScreen gate, the Sidebar
|
||||
(`AppShell.tsx:314-323`), StatusBar (`AppShell.tsx:303-309`), ChatHost, all overlays, and the
|
||||
onboarding takeover (`AppShell.tsx:285-296`). A sign-in screen must show **none of that**.
|
||||
Mounting `/auth` as a child of `path="/"` would draw the whole authenticated chrome behind
|
||||
the login form.
|
||||
|
||||
### Recommended shape (sibling route, outside the shell)
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route path="/auth" element={<AuthRoute />} /> {/* NEW — sibling, pre-shell */}
|
||||
<Route path="/" element={<AppShell />}>
|
||||
…existing 28 children…
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
- `AuthRoute` is a **standalone full-screen component** — it MUST NOT use `SurfaceBoundary`
|
||||
(its `onClose → navigate('/home')` assumes an authenticated home, `SurfaceBoundary.tsx:13`).
|
||||
Wrap in a plain `AppErrorBoundary` if any, with `onClose → window.location.reload()` (the
|
||||
same pattern `App.tsx:61` uses at the root).
|
||||
- Theme still applies: `data-theme` is set pre-paint in `main.tsx` (per BUILD-PLAN §4) and
|
||||
`<ThemeProvider>` wraps `<BrowserRouter>` at `App.tsx:53`, so `/auth` inherits warm tokens
|
||||
even though it's outside AppShell. Good — no extra wiring needed for theming.
|
||||
|
||||
### Honesty gate (CRITICAL — auth is the #1 fabrication risk)
|
||||
There is **no real user-identity auth in the product today.** What exists is a **same-origin
|
||||
local sidecar session-token** (`adapter.ts:295-308` `fetchSessionToken`, refreshed at
|
||||
`adapter.ts:316-331`; server route `local/index.ts:2047` `/api/auth/session-token`, auth-exempt
|
||||
`security-middleware.ts:238`). That is a *dev/desktop bootstrap Bearer*, NOT a logged-in human.
|
||||
The only "Clerk-gated" surface is the **CLOUD** server (`local/routes/agents.ts:12` comment:
|
||||
"`/api/agents/*` CRUD exists only on the Clerk-gated CLOUD server"), which the local app does
|
||||
not run.
|
||||
|
||||
So `/auth` is **EXTERNAL-DEP / MUST-BUILD**: real Clerk components need a `CLERK_PUBLISHABLE_KEY`
|
||||
+ the `@clerk/clerk-react` provider (neither present — zero `@clerk` imports in `apps/web/src`).
|
||||
**Until that wiring is real, the auth screen must NOT show a fake signed-in identity, a fake
|
||||
name/avatar, or pretend a session exists.** The design's own framing helps here: SCREENS §13
|
||||
(`SCREENS.md:271-278`) says "An account is optional — Waggle runs fully local without one" and
|
||||
"Continue routes to Home." A PR7-honest auth screen can render the themed Clerk UI but, with no
|
||||
key configured, must degrade to the local-first "continue without an account → Home" path
|
||||
rather than inventing a logged-in user. (The sidebar user row already degrades to "Account" +
|
||||
"W" avatar when `getIdentity()` returns no name — `AppShell.tsx:98-109`, PR1 LOW #2 — so the
|
||||
"no real identity" state is already an accepted, non-fabricated UI.)
|
||||
|
||||
The `clerk-setup` / `clerk-react-patterns` / `clerk-billing` skills are available for the build PR.
|
||||
|
||||
---
|
||||
|
||||
## 5. `/billing` — reuse the EXISTING Settings "Plan" tab (do not rebuild from zero)
|
||||
|
||||
### What is already REAL (verified, fully wired)
|
||||
- **Settings "Plan" tab** — `SettingsApp.tsx:41` (`{ id:'billing', label:'Plan', icon:DollarSign }`);
|
||||
renders at `SettingsApp.tsx:511-655` (`activeTab === 'billing'`).
|
||||
- **`useBilling` hook** — `hooks/useBilling.ts:24-124`: `startCheckout('PRO'|'TEAMS')`
|
||||
(`useBilling.ts:69-81`), `openPortal()` (`useBilling.ts:84-96`), `syncAfterCheckout`
|
||||
(`useBilling.ts:49-66`), auto-detects `?session_id=` on return (`useBilling.ts:104-115`).
|
||||
P1b honesty already baked in: `tierResolved=false` until a real `getTier()` succeeds; the
|
||||
default 'FREE' is NEVER shown as fact (`useBilling.ts:13-22, 38-43`; rendered unresolved
|
||||
state at `SettingsApp.tsx:525-536`).
|
||||
- **Adapter Stripe layer** — `adapter.ts:2658-2680`: `syncStripeCheckout`,
|
||||
`createCheckoutSession`, `createPortalSession`.
|
||||
- **Server Stripe routes (REAL)** — `packages/server/src/stripe/{checkout,portal,sync,webhook}.ts`.
|
||||
`checkout.ts` returns **`503 STRIPE_NOT_CONFIGURED`** when no `STRIPE_SECRET_KEY`
|
||||
(`checkout.ts:19-20`); success/cancel URLs are `/payment-success?session_id=…` and
|
||||
`/payment-cancelled` (`checkout.ts:42-43`). Webhook handler + 4 price-var resolution shipped
|
||||
per CLAUDE.md §10 (E-10).
|
||||
- **CoverageCompassCard** value-prop card already renders above the tier card
|
||||
(`SettingsApp.tsx:520`).
|
||||
|
||||
### What is a build GAP for the SCREENS §14 design (`SCREENS.md:282-295`)
|
||||
| §14 design element | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Plans state (3 cards, Pro popular) | DERIVABLE — upgrade buttons exist | `SettingsApp.tsx:579-619` |
|
||||
| **Monthly / annual toggle (−20%)** | **MUST-BUILD on FE** — server already accepts `billingPeriod` (`checkout.ts:14,29`) but adapter `createCheckoutSession(tier)` does **not pass it** (`adapter.ts:2667`); `useBilling.startCheckout` has no period arg (`useBilling.ts:69`) | grep: zero `billingPeriod`/`annual`/`monthly` in adapter+useBilling |
|
||||
| Checkout state (card form) | EXTERNAL-DEP — use **Stripe Checkout** (hosted), per SCREENS §14 "Use Stripe Checkout/Customer Portal where possible" (`SCREENS.md:294`) | `checkout.ts` creates hosted `session.url` |
|
||||
| Success state | **MUST-BUILD route** — `success_url` points at `/payment-success` which **does not exist** as a route (grep: 0 hits in App.tsx); today only `?session_id=` is read by `useBilling.ts:104-115` on whatever page is mounted | `checkout.ts:42` vs App.tsx |
|
||||
| Manage state (portal, invoices, PDF) | REAL — "Manage Subscription" → `openPortal()` → Stripe Customer Portal (`SettingsApp.tsx:621-635`, `useBilling.ts:84-96`). **Invoices/PDF are inside Stripe's portal, not our UI** — do NOT render fake invoice rows in-app. |
|
||||
|
||||
### Recommended registration for billing
|
||||
- **Primary:** keep billing as the **Settings → Plan tab**, reskinned to warm tokens + the
|
||||
§14 segmented 4-state layout. Add the **`/payment-success`** route (and optionally
|
||||
`/payment-cancelled`) — these CAN be AppShell children using the PR6 wrapper pattern (the
|
||||
user is back inside the app post-checkout), OR a tiny standalone confirmation. Wire ⌘K
|
||||
`upgrade`/`settings` entries to deep-link the Plan tab.
|
||||
- **Optional standalone `/billing`:** only if design wants the full Plans/Checkout/Success/Manage
|
||||
flow outside Settings chrome. If so, follow the §2 PR6 wrapper pattern (AppShell child —
|
||||
billing IS post-login, unlike auth). Reuse `useBilling` verbatim; do not duplicate Stripe calls.
|
||||
|
||||
### BYO-vs-metered (BLOCKER — founder decision before billing ships)
|
||||
BUILD-PLAN §7 item 5 (`BUILD-PLAN.md:165-166`) and DESIGN_POV §4 (`DESIGN_POV.md:62-70`) flag
|
||||
**who pays for inference (BYO-key vs Waggle-metered)** as the decision that "quietly reshapes
|
||||
Billing, Onboarding, and Usage" and "should be settled before Billing goes live"
|
||||
(`DESIGN_POV.md:89-90`). This is a **decision gate for PR7**, not a code question — the current
|
||||
billing tab supports either ("supports either but commits to neither", `DESIGN_POV.md:70`).
|
||||
|
||||
---
|
||||
|
||||
## 6. The breadcrumb / `matchNavRoute` label gap (must NOT repeat for /billing)
|
||||
|
||||
### How the breadcrumb label is derived
|
||||
`AppShell.tsx:224-229`:
|
||||
```ts
|
||||
const labelEntries = flattenAppEntries(getDockForTier('power', billingTier)); // 224
|
||||
const activeRoute = matchNavRoute(location.pathname, labelEntries.map(e=>e.route)…); // 225
|
||||
const surfaceLabel = labelEntries.find(e => e.route === activeRoute)?.label ?? null; // 229
|
||||
```
|
||||
`surfaceLabel` is passed to `<StatusBar focusedWindowLabel={surfaceLabel}>`
|
||||
(`AppShell.tsx:304`). `matchNavRoute` (`routes.ts:146-154`) is a longest-prefix match against
|
||||
the **dock-tiers route table only**.
|
||||
|
||||
### The gap (this is the handoff "P3 / PR6 ⌘K-only routes show a fuzzy fallback label" note)
|
||||
`dock-tiers.ts` `POWER_CONFIG` (`dock-tiers.ts:64-118`) does **NOT contain `/benchmarks` or
|
||||
`/platform`** (confirmed by grep — zero hits in dock-tiers.ts). So for those routes
|
||||
`matchNavRoute` returns `null` → `surfaceLabel = null` → **the StatusBar breadcrumb simply
|
||||
does not render** (it is gated `{focusedWindowLabel && (…)}` at `StatusBar.tsx:84`). The
|
||||
PR6 ⌘K-only surfaces therefore show **no breadcrumb at all** (not literally a wrong/fuzzy
|
||||
string — the label is null and the breadcrumb chip is hidden). Either way the surface is
|
||||
unlabeled in the status bar.
|
||||
|
||||
### Requirement for PR7
|
||||
**`/billing` (and `/payment-success`, and conceptually `/auth`) MUST get a breadcrumb label
|
||||
so they don't repeat the unlabeled-surface gap.** The label map (`labelEntries`) is sourced
|
||||
ONLY from `getDockForTier(...)` — i.e. from `dock-tiers.ts`. Two options:
|
||||
1. **Add a dock-tiers entry** (with `route` + `label`) for the billing surface so
|
||||
`matchNavRoute` resolves it — same fix PR6 should have applied to benchmarks/platform. But
|
||||
billing lives under `/settings` today (Settings already has a dock entry `dock-tiers.ts:108`,
|
||||
label "Settings"), so a `?tab=billing` deep-link inherits the "Settings" breadcrumb already
|
||||
— acceptable. A standalone `/billing` would need its own entry.
|
||||
2. **`/auth` needs NO breadcrumb** — it renders outside AppShell (§4), so `AppShell.tsx`'s
|
||||
StatusBar never mounts for it. The gap is irrelevant for auth by construction.
|
||||
|
||||
> Net: the breadcrumb gap is an AppShell-internal concern. `/auth` sidesteps it (no shell).
|
||||
> `/billing` should either ride the existing "Settings" entry (deep-link path) or, if
|
||||
> standalone, add a `dock-tiers.ts` route+label entry — do **not** ship it label-less.
|
||||
|
||||
### Deep-link gap that BLOCKS the "/settings?tab=billing" approach (verified)
|
||||
`SettingsApp` initializes `activeTab` to **`'models'`** (`SettingsApp.tsx:53`) and has **NO
|
||||
`?tab=` / `useSearchParams` reader** (grep: zero `tab=`/`useSearchParams`/`searchParams`/
|
||||
`initialTab` in `SettingsApp.tsx`). So today `/settings?tab=billing` and even the existing
|
||||
`APP_ROUTES.backup = '/settings?tab=backup'` (`routes.ts:52`) **silently open on Models, not the
|
||||
requested tab.** For PR7 to deep-link billing from ⌘K / Upgrade buttons, SettingsApp needs a
|
||||
small **`?tab=` initializer** (read once on mount, snap `activeTab`). This is a real, small
|
||||
MUST-BUILD — without it the catalog "Upgrade to Pro" / UpgradeModal-fallback `navigate('/settings')`
|
||||
(`AppShell.tsx:411-413, 422`) lands a user on Models, not Plan.
|
||||
|
||||
---
|
||||
|
||||
## 7. Existing upgrade entry points (where "Upgrade"/"Manage plan" link today)
|
||||
|
||||
| Surface | Action | Target | Evidence |
|
||||
|---|---|---|---|
|
||||
| Settings → Plan tab, FREE/TRIAL | Pro / Teams cards | `billing.startCheckout('PRO'\|'TEAMS')` → Stripe Checkout | `SettingsApp.tsx:583-598` |
|
||||
| Settings → Plan tab, PRO | "Upgrade to Teams" | `startCheckout('TEAMS')` | `SettingsApp.tsx:604-619` |
|
||||
| Settings → Plan tab, PRO/TEAMS | **"Manage Subscription"** | `billing.openPortal()` → Stripe Customer Portal | `SettingsApp.tsx:621-635` |
|
||||
| Settings → Plan tab, non-ENT | Enterprise CTA | external link `https://www.kvark.ai` | `SettingsApp.tsx:638-652` |
|
||||
| `UpgradeModal` overlay | `onUpgrade(tier)` | `adapter.createCheckoutSession(...)`, **fallback `navigate('/settings')`** | `AppShell.tsx:404-415` |
|
||||
| `TrialExpiredModal` overlay | `onUpgrade(tier)` | `adapter.createCheckoutSession(...)`, fallback `navigate('/settings')` | `AppShell.tsx:417-424` |
|
||||
| ⌘K catalog | "Upgrade to Pro" / "Settings" | `to:"/settings"` | `command-catalog.ts:75-76` |
|
||||
| Sidebar user row | tier label "Trial · 9d" / "Pro" | (display only; row → Settings) | `AppShell.tsx:264-269, 321-322` |
|
||||
|
||||
All upgrade paths converge on Stripe checkout (real) or land on `/settings` (which mis-opens
|
||||
on Models per §6). The "Manage plan" affordance is the existing **"Manage Subscription"**
|
||||
button → Stripe Customer Portal. There is **no `KvarkNudge` component in `apps/web/src`**
|
||||
(grep: 0 hits; CLAUDE.md §9 references it but it is not in the web app today — Enterprise CTA
|
||||
is the inline kvark.ai link at `SettingsApp.tsx:644-651`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Fabrication risks for PR7 (gate-off candidates — HONESTY CONTRACT)
|
||||
|
||||
1. **A logged-in identity that isn't real.** No Clerk/user-auth exists; the session-token is a
|
||||
local dev Bearer. The auth screen must not render a fake signed-in user/name/avatar or
|
||||
claim a session. Degrade to the design's local-first "continue without an account" path
|
||||
when no `CLERK_PUBLISHABLE_KEY` is configured. (`adapter.ts:295-308`, `local/index.ts:2047`,
|
||||
`AppShell.tsx:98-109` accepted "Account" fallback.)
|
||||
2. **Fake invoices / PDFs.** SCREENS §14 lists "invoices (Paid + PDF)". Those live inside the
|
||||
**Stripe Customer Portal**, not our UI. Do NOT render invented invoice rows or fake
|
||||
"Download PDF" links in-app — route to `openPortal()` (`useBilling.ts:84-96`).
|
||||
3. **Fake payment method ("VISA ···4242").** That `…4242` string in SCREENS §14 is design
|
||||
filler. Real card-on-file data lives in Stripe's portal. Do not display a hardcoded masked
|
||||
card in the Manage state.
|
||||
4. **Fake "next charge" / usage / due-today numbers.** SCREENS §14 Checkout shows "Due today
|
||||
$19 / won't be charged until …". Those must come from the real Stripe session, not be
|
||||
string-literal'd. Prefer hosted Stripe Checkout (`checkout.ts:39-50`) which renders the real
|
||||
amounts itself.
|
||||
5. **Presenting tier as fact before it resolves.** Already guarded by `tierResolved`
|
||||
(`useBilling.ts:13-22`); PR7 must preserve that — never show "Free plan" as fact while
|
||||
unresolved (`SettingsApp.tsx:525-536`).
|
||||
6. **A fake monthly/annual price.** The −20% annual toggle is a build gap (§5); when added it
|
||||
must resolve through a real annual price var (server `priceIdForTier(tier, 'annual')`,
|
||||
`checkout.ts:29`) — not a client-side `$19 × 0.8` cosmetic number that doesn't match what
|
||||
Stripe charges.
|
||||
|
||||
---
|
||||
|
||||
## 9. File index (everything PR7 routing touches)
|
||||
|
||||
| Concern | File:line |
|
||||
|---|---|
|
||||
| Route table (add `/auth` sibling, optional `/billing` child, `/payment-success`) | `apps/web/src/App.tsx:52-110` |
|
||||
| Route-wrapper pattern to copy | `apps/web/src/routes/BenchmarkRoute.tsx:1-11`; `routes/SurfaceBoundary.tsx:10-17` |
|
||||
| Barrel | `apps/web/src/routes/index.ts:33-59` |
|
||||
| AppId→URL table (`/auth`,`/billing` are NOT here yet; `backup` deep-link precedent) | `apps/web/src/lib/routes.ts:25-53, 70-80`; `matchNavRoute` `routes.ts:146-154` |
|
||||
| ⌘K catalog (retarget upgrade entry) | `apps/web/src/lib/command-catalog.ts:62-97` |
|
||||
| Shell breadcrumb derivation + label-source gap | `apps/web/src/components/os/AppShell.tsx:224-229, 304`; `StatusBar.tsx:84-93` |
|
||||
| Dock label table (no benchmarks/platform/billing entries) | `apps/web/src/lib/dock-tiers.ts:64-118` |
|
||||
| Existing Billing surface (REUSE) | `apps/web/src/components/os/apps/SettingsApp.tsx:36-47, 511-655` |
|
||||
| Billing hook (REUSE) | `apps/web/src/hooks/useBilling.ts:24-124` |
|
||||
| Adapter Stripe + session-token | `apps/web/src/lib/adapter.ts:2658-2680, 295-331` |
|
||||
| Server Stripe routes | `packages/server/src/stripe/{checkout,portal,sync,webhook}.ts` (checkout `checkout.ts:13-58`) |
|
||||
| Server local session-token (only "auth" today) | `packages/server/src/local/index.ts:2043-2047`; `security-middleware.ts:238` |
|
||||
| SCREENS specs | `docs/design_handoff_waggle_app/SCREENS.md:269-295` |
|
||||
| BYO-vs-metered decision gate | `docs/design_handoff_waggle_app/DESIGN_POV.md:62-70, 89-90`; `BUILD-PLAN.md:165-166` |
|
||||
166
docs/redesign-warm-hive/pr7-recon/07-byo-vs-metered.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# PR7 Recon — DESIGN_POV #4: BYO-key vs Waggle-metered (the inference-cost decision)
|
||||
|
||||
> RECON ONLY. No product code changed. Every claim below is grounded with `file:line`.
|
||||
> Topic: make the founder's BYO-vs-metered choice **concrete with build cost**, grounded in
|
||||
> what already shipped through PR1–PR6. PR7 = screen 13 Auth (Clerk) + screen 14 Billing (Stripe)
|
||||
> per `docs/redesign-warm-hive/BUILD-PLAN.md §6` (line 145).
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR (the de-facto commitment)
|
||||
|
||||
**The codebase has already committed to Option A: BYO-key + flat SUBSCRIPTION tiers, with no inference metering.** This isn't a design intention — it is *shipped, wired, and tested* across three surfaces:
|
||||
|
||||
1. **BYO-key is the only inference-payment path that exists.** Onboarding's hard model gate and Settings → Models both mount the same `ModelGate` whose entire copy is *"Bring your own key — it's stored encrypted in your Vault and never leaves your machine."* (`apps/web/src/components/os/model-gate/ModelGate.tsx:186`). Waggle never holds an inference key or pays a provider on the user's behalf in any shipped path.
|
||||
2. **Stripe billing is flat `mode: 'subscription'`** — both the desktop sidecar (`packages/server/src/stripe/checkout.ts:40`) and the `apps/www` cloud port (`apps/www/app/api/stripe/checkout/route.ts:170`). Two products (Pro $19, Teams $49/seat), monthly/annual, period-end renewal. **No `mode: 'payment'`, no usage records, no metered prices.**
|
||||
3. **There is zero inference-metering plumbing.** No `createUsageRecord`, no `billing_meter`, no `reportUsage`, no credits/balance ledger anywhere in `packages/server/src` (verified by grep — only hit is a *comment* about not burning Anthropic credits in `packages/server/src/local/index.ts:1980`). The only "usage" surface is an **estimate-based read-only cost dashboard** (`/api/cost/summary`) with a **soft, advisory** daily-budget warning — never a hard cap, never tied to billing.
|
||||
|
||||
**Recommendation: ratify Option A.** PR7 Billing becomes a *theming* task over an already-working subscription flow (near-zero new backend). Option B (Waggle-metered) is a multi-month strategic pivot touching billing, onboarding, the inference path, quota enforcement, and a new credits surface — and it contradicts the local-first / "your key never leaves your machine" promise the product already makes to users in onboarding copy. The recon's job is to make this choice concrete; the decision is the founder's.
|
||||
|
||||
---
|
||||
|
||||
## 1. Evidence — what PR5 actually shipped for BYO-key (D1)
|
||||
|
||||
**`ModelGate.tsx` is the single shared "get a working model" component** — mounted in onboarding step 3 AND Settings → Models (`apps/web/src/components/os/model-gate/ModelGate.tsx:8–20` header doc).
|
||||
|
||||
- BYO-key cloud path: pick provider → paste key → **live-validate** (`adapter.testApiKey(..., { live: true })`, line 94) → write to Vault (`adapter.setProviderKey`, line 99).
|
||||
- Explicit BYO framing in the UI: *"Bring your own key — it's stored encrypted in your Vault and never leaves your machine."* (line 186).
|
||||
- Honesty contract already enforced: "verified" only after a live probe; a format-only pass says "looks valid (not live-verified)" (lines 18–20, 247–253).
|
||||
- Local path (Ollama) is the other route to a working model — also zero cost to Waggle (lines 266–304).
|
||||
|
||||
**Onboarding hard gate (D2):** `ModelGateStep.tsx` disables "Continue" until `useHasWorkingModel` is true (`apps/web/src/components/os/overlays/onboarding/ModelGateStep.tsx:49`), with copy *"Bring your own provider key … Nothing leaves your machine without your key."* (lines 26–28) and one soft escape ("I'll do this later" → Home `NoModelBanner`, lines 9–17).
|
||||
|
||||
**Interpretation:** the user pays the provider directly. Waggle's margins are clean; it never carries inference cost. This is textbook **Option A (BYO-key)** from DESIGN_POV §4 (`docs/design_handoff_waggle_app/DESIGN_POV.md:62–70`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Evidence — Stripe tiers are flat SUBSCRIPTION, not metered
|
||||
|
||||
**`packages/shared/src/tiers.ts`** — canonical 5-tier system (TRIAL/FREE/PRO/TEAMS/ENTERPRISE), pricing in the header doc (lines 7–18): flat per-seat/per-month dollar amounts. `stripePriceId` is a single price per tier (lines 122, 143) — a **fixed recurring price**, not a metered/usage price.
|
||||
|
||||
**Desktop sidecar Stripe (`packages/server/src/stripe/`):**
|
||||
- `checkout.ts:40` — `mode: 'subscription'`, `line_items: [{ price: priceId, quantity: 1 }]`. Quantity 1, fixed price. (Only PRO/TEAMS, line 25.)
|
||||
- `webhook.ts:114–158` — handles exactly **3** subscription lifecycle events: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`. **No `invoice.created` / usage-record handling.** On cancel → tier drops to FREE (line 150).
|
||||
- `index.ts:71–100` — `tierFromPriceId` / `priceIdForTier` map fixed monthly/annual price IDs to tiers. Pure subscription mapping.
|
||||
- `portal.ts:41` — `stripe.billingPortal.sessions.create` — defers payment-method / invoice / cancel management to **Stripe's hosted Customer Portal** (this is where invoices and payment methods legitimately come from, never invented locally).
|
||||
|
||||
**`apps/www` cloud port (already built — see §4):**
|
||||
- `app/api/stripe/checkout/route.ts:170` — `mode: 'subscription'`, fixed `priceId` resolved by env or `lookup_key` `${tier}_${billing}` (lines 109–119).
|
||||
- `app/api/webhooks/stripe/route.ts:196–205` — same 3 subscription events, mirrored to Clerk `publicMetadata`. No metering.
|
||||
|
||||
**Conclusion:** there is no metered/usage-based Stripe billing anywhere. The model is "pay a flat monthly fee for *capabilities* (workspaces, connectors, governance), not for *inference*." Inference is on the user's own key/quota.
|
||||
|
||||
---
|
||||
|
||||
## 3. Evidence — the current Usage/cost surface (what's shown today)
|
||||
|
||||
**There is NO `UsageApp.tsx`.** The de-facto Usage screen is `TelemetryApp.tsx` — titled **"Usage & cost"** in the UI (`apps/web/src/components/os/apps/TelemetryApp.tsx:159`). PR6b's "Usage/budget" screenshot (`docs/redesign-warm-hive/smoke-pr6b-20260618/04-usage-budget.png`) is this surface.
|
||||
|
||||
What it shows (all **read-only, estimate-based** — never a balance to draw down):
|
||||
- Total tokens, **estimated** cost, by-model spend, by-workspace (TEAMS-gated) — from `GET /api/cost/summary` and `/api/cost/by-workspace` (`TelemetryApp.tsx:55–100`).
|
||||
- A **daily budget** the user can set, which produces a **soft warning at 80% / "exceeded"** status (`TelemetryApp.tsx:151`, `192–195`) — purely advisory.
|
||||
|
||||
The backing route confirms the "estimate, not meter, not enforce" nature:
|
||||
- `packages/server/src/local/routes/cost.ts:8–9` — *"Data source: in-memory CostTracker … All cost values are **estimates** based on published model pricing."*
|
||||
- Returns `estimatedCost` everywhere (lines 175, 181, 188).
|
||||
- The daily-budget "exceeded" status (lines 162–169) sets a **string status only** — nothing in the codebase blocks a request when exceeded. It's a dashboard, not a quota gate.
|
||||
- Free for all tiers per a product decision (line 263, "P22 … usage/telemetry info is free for all tiers").
|
||||
|
||||
**Interpretation:** today's Usage tells the user *"here's roughly what your own provider key is costing you"* — a BYO-key courtesy readout. It is structurally NOT a metered-balance/credits surface.
|
||||
|
||||
---
|
||||
|
||||
## 4. Evidence — Auth (Clerk) status: NOT in the desktop app; FULLY built in `apps/www`
|
||||
|
||||
**Desktop `apps/web` has no Clerk and no real logged-in identity.** Grep for `Clerk|@clerk|SignIn|auth0` across `apps/web/src` → **no files**. "Identity" today = a `tier` field in `config.json`, read fail-closed-to-FREE by `readTierFromDataDir` (`packages/server/src/middleware/assert-tier.ts:21–32`). There is a *data-model placeholder*: `User.clerkId: string` exists in `packages/shared/src/types.ts:6`, but nothing populates it from a real Clerk session in the desktop path.
|
||||
|
||||
**`apps/www` (Next.js cloud/landing) already has a complete, themed Clerk + Stripe SaaS surface** — this is the direct reference (and possibly the literal home) for PR7's screens 13/14:
|
||||
- `apps/www/app/sign-in/[[...sign-in]]/page.tsx` — `<SignIn />` Clerk component, themed via `<ClerkProvider>` (header doc line 15).
|
||||
- `apps/www/app/sign-up/[[...sign-up]]/page.tsx` — sign-up (verified to exist via glob).
|
||||
- `apps/www/middleware.ts:6,16` — `clerkMiddleware()` wired, matcher includes API routes.
|
||||
- `apps/www/app/account/page.tsx` — account surface.
|
||||
- `apps/www/app/api/stripe/checkout/route.ts` — lazy-create Stripe Customer → store id in Clerk `publicMetadata` (lines 81–100); subscription checkout (line 170).
|
||||
- `apps/www/app/api/webhooks/stripe/route.ts` — mirrors subscription state Stripe → Clerk metadata (3 events).
|
||||
- `apps/www/app/_components/Pricing.tsx` — pricing cards.
|
||||
|
||||
**Net for PR7 Auth (screen 13):** in `apps/www`, Auth is REAL and only needs **theming to the warm-Hive tokens**. In the desktop `apps/web`, Auth is **MUST-BUILD if** the desktop must show a real logged-in identity (otherwise the design's own line "An account is optional — Waggle runs fully local without one" — `SCREENS.md:274` — means desktop can stay identity-light and route account/billing to the cloud `apps/www`). **This is itself a sub-decision the founder should confirm: does screen 13 live in `apps/www` only, or also in the desktop shell?**
|
||||
|
||||
---
|
||||
|
||||
## 5. The screen-14 fabrication risks (honesty contract carried from PR3–PR6)
|
||||
|
||||
`SCREENS.md:282–295` (screen 14 Billing) calls for four states. Three of them name fields that **must come from Stripe, never be invented**:
|
||||
|
||||
| Field in the design | Risk | Required gating |
|
||||
|---|---|---|
|
||||
| **Invoices (Paid + PDF)** (`SCREENS.md:292`) | Fabricating an invoice list / fake PDFs | Source ONLY from Stripe Customer Portal (`portal.ts` already does this) — do NOT render a local invoice list. If portal isn't reachable, show "Manage in Stripe" link, not a stub table. |
|
||||
| **Payment method "VISA ···4242"** (`SCREENS.md:291`) | Hardcoding a fake card (the `4242` test card is literally in the spec text) | Never render a card brand/last4 the app doesn't have from Stripe. The portal owns this. The `4242…` in the design is a *mockup placeholder* — it must not ship as real-looking data. |
|
||||
| **"Next charge" / billing cycle** (`SCREENS.md:291–292`) | Inventing a renewal date | Only from Stripe subscription data via the portal. |
|
||||
| **Checkout card form (email/card/expiry/CVC)** (`SCREENS.md:287–289`) | Building a *fake* in-app card form that collects nothing real | Use **Stripe Checkout** (hosted) — the design itself says "Use Stripe Checkout/Customer Portal where possible" (`SCREENS.md:294`). The in-app form mock is illustrative; real PCI capture is Stripe's. |
|
||||
| **Logged-in identity / avatar+name** (screen 13) | Showing a name/email for a session that isn't real | Bind to the real Clerk session (`apps/www`) or render the honest "no account / local-first" state (`SCREENS.md:274`). The desktop's `userName={null}` → "Account"/"W" pattern is the honest fallback (BUILD-PLAN §9 deferred note, line 191). |
|
||||
|
||||
**Plus a Usage-screen trap** if Option B is ever pursued: a credits/balance number, a "you've used X of Y tokens" quota bar, or a "$N remaining" figure would all be **fabricated** today (no ledger exists). The current estimate-only dashboard (§3) is the honest ceiling — do not dress it up as a metered balance.
|
||||
|
||||
---
|
||||
|
||||
## 6. The decision, made concrete
|
||||
|
||||
### Option A — Ratify BYO-key + flat subscription (RECOMMENDED, de-facto current state)
|
||||
|
||||
PR7 Billing themes the **existing** Stripe subscription flow; near-zero new backend. Exactly what's needed:
|
||||
|
||||
- **Auth (screen 13):**
|
||||
- **Cloud (`apps/www`):** theme the existing `<SignIn/>`/`<SignUp/>` Clerk components + the brand split-panel to warm-Hive tokens. Add the local-first trust copy ("an account is optional"). ~UI-only.
|
||||
- **Desktop (`apps/web`):** confirm whether it needs a real auth surface at all (§4 sub-decision). If "local-first, no account" stands, desktop screen 13 is a *deep-link to the cloud account page* + the honest no-account state — minimal build. If a real desktop session is wanted, that's the one genuine new piece (embed Clerk in the SPA / token bridge) — flag as a scoped add-on, not core to Option A.
|
||||
- **Billing (screen 14):**
|
||||
- **Plans state:** theme to tokens; data already exists (tiers.ts, `useBilling.startCheckout`). Add monthly/annual toggle UI (the −20% annual already exists as price IDs — `index.ts:91–100`).
|
||||
- **Checkout state:** redirect to **Stripe Checkout** (already wired both surfaces). The "in-app card form" from the design ships as a themed *intro/summary*, then hands off to Stripe — no PCI surface built.
|
||||
- **Success state:** `useBilling` already syncs `?session_id=` post-checkout (`useBilling.ts:103–115`). Theme the success ring/receipt; receipt link → Stripe.
|
||||
- **Manage state:** `billing.openPortal()` already exists (`useBilling.ts:84`, `SettingsApp.tsx:625`) → Stripe Customer Portal owns invoices/payment-method/cancel. Theme the entry; do NOT build a local invoice/card UI (§5).
|
||||
- **Usage:** leave the estimate-only "Usage & cost" dashboard as-is; optionally reskin to warm-Hive in the long tail. No metering.
|
||||
- **Net new backend for Option A: essentially none.** Possibly: thread `STRIPE_PRICE_*_ANNUAL` into the desktop checkout UI's monthly/annual toggle (the resolver already supports it — `index.ts:91`), and (if desktop auth is wanted) a Clerk-session bridge. Otherwise pure theming + wiring existing routes to the new screens.
|
||||
|
||||
### Option B — Pivot to Waggle-metered (MAJOR ARC, strategic reversal)
|
||||
|
||||
Enumerated NEW plumbing (none of this exists today):
|
||||
|
||||
1. **Inference-cost metering per request** — a real, persisted, authoritative usage ledger (today's CostTracker is **in-memory + estimate-only**, `cost.ts:8`; it would need to become durable, exact, and per-user/account).
|
||||
2. **Waggle holds the provider keys** — a managed model pool where Waggle's own key pays the provider. This **directly contradicts** shipped onboarding/Settings copy ("your key never leaves your machine") and the local-first promise — a product-positioning reversal, not just code. (`managedModelPool` capability exists as a *flag* in tiers.ts:97/130 but has no inference-path implementation behind it.)
|
||||
3. **Usage caps / quota enforcement** — convert the *advisory* budget (`cost.ts:162–169`, soft warning only) into a **hard gate** that blocks chat requests at the inference path when a balance/quota is exhausted. New enforcement point in the agent loop.
|
||||
4. **Stripe metered/usage-based billing** — `mode: 'payment'` top-ups or metered subscription items + `createUsageRecord`/billing-meter reporting. New webhook events (`invoice.created`, usage aggregation). None of the current 3-event handlers (`webhook.ts`) cover this.
|
||||
5. **A credits/balance surface** — a new "$N remaining / buy more credits" screen + the ledger behind it. (None exists; building it without the ledger would be fabrication — §5.)
|
||||
6. **Usage screen rework** — from "here's your own-key estimate" to "here's your metered balance, draw-down, and top-up" — a full rebuild of `TelemetryApp`.
|
||||
7. **Margin/abuse controls** — rate limits, anti-abuse, cost-of-goods accounting that the BYO model never needed because Waggle carried no inference cost.
|
||||
|
||||
This is a multi-month arc that reshapes Billing, Onboarding (the model gate would invert — from "add your key" to "you're metered"), Usage, and the core inference path, and it takes on inference COGS + abuse risk that the current architecture deliberately avoids.
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Ratify Option A.** Rationale: (1) the codebase has *already committed* to it end-to-end (BYO-key gate + flat subscription + no metering), so A is "finish what's shipped," (2) it keeps the local-first / "your key never leaves your machine" promise the product *already makes to users in onboarding*, (3) it keeps margins clean (no inference COGS), and (4) PR7 collapses to theming + wiring existing routes. Option B is a deliberate strategic pivot with real COGS, abuse surface, and a contradiction of live product copy — worth a separate, founder-led decision, **not** something PR7 should absorb. DESIGN_POV §4 said "the current design supports either but commits to neither" (`DESIGN_POV.md:70`); the *implementation* has since committed to A. PR7 should make that commitment explicit and themed.
|
||||
|
||||
---
|
||||
|
||||
## 7. Open sub-decisions for the founder (surfaced, not decided)
|
||||
|
||||
1. **Does screen 13 (Auth) live in `apps/www` only, or also in the desktop `apps/web`?** Desktop has no Clerk today; the design says accounts are optional. If desktop stays identity-light, PR7 desktop-Auth is a deep-link + honest no-account state (cheap). If a real desktop session is wanted, add a scoped Clerk-bridge task.
|
||||
2. **Monthly/annual toggle on the desktop Billing tab** — the annual price resolver already exists (`index.ts:91`); the desktop UI currently only calls `startCheckout('PRO'|'TEAMS')` with default monthly (`SettingsApp.tsx:584`). Adding the toggle is small but is genuinely new desktop UI.
|
||||
3. **Where does screen 14 Billing render?** The richest, already-real flow is in `apps/www` (Clerk-linked). The desktop SettingsApp Billing tab is a thinner subscription surface. PR7 could (a) theme both, or (b) make desktop Billing a deep-link to the cloud account page. Confirm.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — files read for this recon (all `file:line` claims above traceable to these)
|
||||
|
||||
- `docs/design_handoff_waggle_app/DESIGN_POV.md` (§4, lines 62–70)
|
||||
- `docs/redesign-warm-hive/BUILD-PLAN.md` (§6 PR7 row line 145; §7 #5 line 165; §9 deferred note line 191)
|
||||
- `docs/design_handoff_waggle_app/SCREENS.md` (screen 13 lines 269–278; screen 14 lines 282–295)
|
||||
- `packages/shared/src/tiers.ts` (5 tiers, single stripePriceId per tier)
|
||||
- `packages/shared/src/types.ts:6` (`User.clerkId` placeholder)
|
||||
- `packages/server/src/stripe/{index,checkout,webhook,portal}.ts` (subscription-only)
|
||||
- `packages/server/src/middleware/assert-tier.ts` (config.json tier, no real identity)
|
||||
- `packages/server/src/local/routes/cost.ts` (estimate-only, advisory budget)
|
||||
- `apps/web/src/components/os/model-gate/ModelGate.tsx` (BYO-key, shipped)
|
||||
- `apps/web/src/components/os/overlays/onboarding/ModelGateStep.tsx` (hard gate)
|
||||
- `apps/web/src/components/os/apps/TelemetryApp.tsx` ("Usage & cost" surface)
|
||||
- `apps/web/src/hooks/useBilling.ts` (checkout/portal/sync, subscription)
|
||||
- `apps/web/src/components/os/apps/SettingsApp.tsx` (Billing tab, upgrade/portal)
|
||||
- `apps/www/{middleware.ts, app/sign-in/.../page.tsx, app/api/stripe/checkout/route.ts, app/api/webhooks/stripe/route.ts}` (Clerk + Stripe subscription, already built — PR7 reference)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Root-cause: "file-chooser storm" during the PR1 live smoke
|
||||
|
||||
**Verdict: dev/automation-only artifact. NOT a user-facing bug, NOT caused by PR1, no code fix warranted.**
|
||||
|
||||
## Symptom
|
||||
During the PR1 Playwright smoke, navigating a **freshly-started** Vite dev server
|
||||
(`localhost:8081/?skipOnboarding=true`) surfaced 5+ pending "file chooser" modal
|
||||
states on load, blocking screenshots until each was cancelled
|
||||
(`fileChooser.setFiles(undefined)`). 13 console errors also appeared.
|
||||
|
||||
## Investigation (systematic-debugging skill)
|
||||
|
||||
**Phase 1 — static analysis (exhaustive):**
|
||||
- Enumerated every `<input type="file">` in `apps/web/src`: `BackupApp` (×2),
|
||||
`SettingsApp`, `ChatApp`, `FilesApp`/`FileActions`, `ImportStep`, `HarvestTab`.
|
||||
**All are hidden and clicked only via an explicit user-button `onClick`.** None
|
||||
mount on `/home`.
|
||||
- Enumerated every `.click()` call in `apps/web/src`: all are either `<a download>`
|
||||
anchor exports (fire a `download` event, not a file chooser) or the user-bound
|
||||
file-input clicks above. **No `useEffect`/mount-time file-chooser trigger exists.**
|
||||
- `?skipOnboarding=true` bypass only writes `{completed:true, tier:'power'}` to
|
||||
localStorage — no file interaction.
|
||||
|
||||
**Phase 3 — reproduction (decisive):**
|
||||
- Re-ran the same URL against the **steady-state** Vite (`:8080`, deps already
|
||||
optimized): landed cleanly on `/home` with **no file-chooser modal state and no
|
||||
error flood**.
|
||||
- DOM inspection on `/home`: **`input[type=file]` count = 0**, `activeElement = BODY`.
|
||||
|
||||
## Root cause
|
||||
The artifact is tied to Vite's **first-run dependency re-optimization** on a cold dev
|
||||
server, which forces a mid-render full-page reload while Playwright is driving the
|
||||
page. Under that transient + automation, Playwright surfaced phantom file-chooser
|
||||
pending-operations. It does not occur on a warm dev server, in the production build,
|
||||
or in the Tauri desktop binary — and there is no application code path that opens a
|
||||
file dialog without a user gesture (which real browsers block anyway).
|
||||
|
||||
The 13 console errors were a separate smoke-setup artifact: the `:8081` frontend
|
||||
calling the founder's `:8080`/`:3333` sidecar with a mismatched session (cross-instance auth).
|
||||
|
||||
## Action
|
||||
None on the product. For future smokes: warm the dev server (let dep optimization
|
||||
finish) before driving Playwright, or smoke the production build / an already-running
|
||||
dev server.
|
||||
BIN
docs/redesign-warm-hive/smoke-20260615/warmhive-cmdk-dark.png
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
docs/redesign-warm-hive/smoke-20260615/warmhive-home-dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/redesign-warm-hive/smoke-20260615/warmhive-home-light.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/redesign-warm-hive/smoke-pr3-20260616/01-home-dark.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 486 KiB |
BIN
docs/redesign-warm-hive/smoke-pr3-20260616/03-chat-dark.png
Normal file
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 486 KiB |
55
docs/redesign-warm-hive/smoke-pr3-20260616/SMOKE-RESULTS.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# PR3 live smoke — 2026-06-16
|
||||
|
||||
Live browser smoke of the three warm-Hive PR3 screens against the running dev
|
||||
env (sidecar :3333 + vite :8080, `?skipOnboarding=true` → tier:power), via
|
||||
chrome-devtools. **Result: PASS — all three screens render correctly in dark +
|
||||
light with 0 console errors.**
|
||||
|
||||
## Screens verified
|
||||
|
||||
| # | Screen | Theme | Evidence | Result |
|
||||
|---|--------|-------|----------|--------|
|
||||
| 01 | Home (Editorial) | dark | `01-home-dark.png` | ✅ |
|
||||
| 02 | Workspace (Overview + tabs) | dark | `02-workspace-overview-dark.png` | ✅ |
|
||||
| 03 | Chat (restyle) | dark | `03-chat-dark.png` | ✅ |
|
||||
| 04 | Workspace (Overview) | light | `04-workspace-light.png` | ✅ |
|
||||
|
||||
## What rendered (real data, not mocks)
|
||||
|
||||
**Home (Phase A):** mono date row "TUESDAY, JUNE 16 · 12:23 PM" + live dot;
|
||||
H1 = the real personalized greeting; honey "2 workspaces waiting for you."
|
||||
(composed second line); overnight hero "Overnight, Waggle ran **8** automations."
|
||||
(story composed from the real OvernightSummary count) + "8 automations completed"
|
||||
run chip; "Pick up where you left off" cards (Continue + "1 to review" + kebab);
|
||||
"Waggle suggests" with a composed sub-line; "Up next" schedules; ask bar (+ / ⌘K /
|
||||
Send). Calm spine + "Pinned · power tools" (tier:power). Streak correctly hidden
|
||||
(SHOW_STREAK gate — no fabricated streak).
|
||||
|
||||
**Workspace (Phase C):** breadcrumb "Home › Writer demo — Anya"; 46px hex avatar;
|
||||
H1; meta "9 memories · updated 4d ago"; Memory + Continue buttons + kebab; the
|
||||
**6-tab bar with counts** (Overview · Chat 2 · Memory 9 · Artifacts · Files ·
|
||||
Team 1), honey underline on the active tab; 2-col Overview — left = summary +
|
||||
"What Waggle knows" fact rows (hex check tile + content + **real date**, no
|
||||
fabricated `⬡ source`) + recent work; right = Status (Agent idle · Memories 9) +
|
||||
Up next (blocked + next, "all →") + Team.
|
||||
|
||||
**Chat (Phase B):** restyled **ModelPill** ("Waggle picked the model — click to
|
||||
override"); the new **bot meta line** "Waggle · Writer · claude-sonnet-4-6";
|
||||
rich markdown (tables / blockquotes); the new composer ("Reply, or ask Waggle to
|
||||
take the next step…" + mono "⏎ send · ⌘K" hint + honey send). The ActivityStream
|
||||
+ work-canvas did not appear on this turn because it was a pure text response
|
||||
(no step-blocks, no file-write) — correct behavior; they surface on agentic turns.
|
||||
|
||||
## Theme
|
||||
Light mode verified on Workspace (`04`): warm-paper background, warm graphite
|
||||
text, honey accents (active tab, Continue button, hex avatar, fact-row check
|
||||
tiles) — a pure token swap, no light-specific code (PR1 architecture). 0 console
|
||||
errors in either theme.
|
||||
|
||||
## Console
|
||||
`list_console_messages(error)` returned **no messages** on Home, Workspace, and
|
||||
Chat in both themes.
|
||||
|
||||
## Note
|
||||
The pre-existing `LoginBriefing` Day-0 overlay renders on first Home load (not
|
||||
part of PR3); dismissed via "Start Working". Not a redesign regression.
|
||||
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
43
docs/redesign-warm-hive/smoke-pr35-20260616/SMOKE.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# PR3.5 Memory-Trust — Live Smoke Evidence (2026-06-16)
|
||||
|
||||
Branch `feature/warm-hive-pr3` @ `876737e6`. Live browser smoke via chrome-devtools
|
||||
MCP against the running dev env (FE vite hot-reload on the page origin; API base
|
||||
`:3333`). Screens captured to this dir.
|
||||
|
||||
## Verdict: PASS (UI) — 0 console errors, dark + light
|
||||
|
||||
| # | Screenshot | What it proves |
|
||||
|---|---|---|
|
||||
| 01 | `01-trust-manage-dark.png` | Trust is the **default Memory view**; segmented Manage/Why; editorial hero; 4-stat bar with **honest "—" gating** on "High confidence & fresh" (no confidence in the set); All/Stale/Needs-confirm chips + disabled "Forgotten"; 3 real rows (M-47/M-45/M-1) with ConfidenceRing (—/unknown), `⬡ M-id · source: you · ● fresh`, M-1 = "aging — added 9w ago"; principle footer. |
|
||||
| 02 | `02-trust-why-dark.png` | Why view: hero ("PROVENANCE · ACCOUNTABILITY" + quote H1), honest empty state ("Open a memory's ⬡ trace…"), Why-specific principle. |
|
||||
| 04 | `04-trust-manage-light.png` | Same Manage view in **light** — pure token swap (PR1 architecture), honey accent + warm-light tokens intact. |
|
||||
|
||||
Also verified interactively (a11y snapshots):
|
||||
- Cross-view flow: Manage row → edit drawer (Correct editor + EvidencePanel "source: user_stated" + Save/Why/Forget) → "Why is this here?" switches to the Why view and loads that memory's trace.
|
||||
- a11y: ConfidenceRing `role=img` + aria-label; row buttons aria-labelled; segmented = toggle buttons (aria-pressed); "Forgotten" chip aria-disabled with rationale.
|
||||
- Console: **0 errors / 0 warnings** across Manage, Why, drawer, dark + light.
|
||||
|
||||
## Constraint — live verification of the NEW sidecar routes deferred
|
||||
|
||||
The running `:3333` sidecar (PID 28776) **started 2026-06-12 22:53 — 4 days before
|
||||
PR3.5** (first PR3.5 commit `c4794737`, 2026-06-16 14:13). `tsx` does not hot-reload,
|
||||
so the running sidecar does **not** register PR3.5's new routes. Therefore, live:
|
||||
- `GET /api/memory/:id/trace` → the Why view showed its **error** state ("Not found")
|
||||
for M-1 instead of the honest `{trace:null}` empty state — **because the route is
|
||||
unregistered on the June-12 sidecar, NOT a code bug.** With the route present, M-1
|
||||
(no `trace_id`) returns `{trace:null}` → the "No trace is linked to M-1" empty state.
|
||||
- `POST /api/memory/:id/confirm` and the **Part 1A** `frame.source` projection on the
|
||||
`/api/workspaces/:id/context` route likewise need a fresh sidecar to see live.
|
||||
|
||||
These are all **unit-verified** (memory-center 25/25 incl. /confirm + /trace null/404
|
||||
paths; workspaces.ts projection covered by server tsc) and the FE calls them correctly.
|
||||
**To verify the new routes end-to-end: restart the sidecar from this working tree**, then
|
||||
re-open `/memory` (Trust) and a memory's "Why is this here?".
|
||||
|
||||
## Notes
|
||||
- Rows show `source: you` because the seeded/identity memories are genuinely
|
||||
`user_stated`. The PR3.5-review write-fix stamps NEW agent memories `agent_inferred`,
|
||||
so agent-derived rows will read "agent" once such memories exist.
|
||||
- The two-nav-layer (MemoryCenterApp tab bar + Trust segmented control) and the
|
||||
Hive-DS-vs-warm token seam are a known, documented integration trade (review MED,
|
||||
deferred — broader restyle).
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 819 KiB |
@@ -0,0 +1,85 @@
|
||||
# PR3.5 — Live route verification against a FRESH sidecar (2026-06-16)
|
||||
|
||||
**Why this exists:** the S3 handoff shipped PR3.5 but flagged one open item — the running
|
||||
`:3333` dev sidecar was started 2026-06-12 (4 days pre-PR3.5), so `tsx` never loaded the
|
||||
new routes. The committed UI smoke (`smoke-pr35-20260616/`) therefore ran the Memory-Trust
|
||||
front door against a **stale** backend. This pass restarts the sidecar from the PR3.5 tree
|
||||
and exercises the three new routes end-to-end (HTTP + FE), closing that gap.
|
||||
|
||||
**Verdict: PASS.** All three PR3.5 backend routes work live; the FE wires to them with **0
|
||||
console errors/warnings**.
|
||||
|
||||
---
|
||||
|
||||
## Environment (verified, not assumed)
|
||||
|
||||
| Thing | State |
|
||||
|---|---|
|
||||
| Branch | `feature/warm-hive-pr3` @ `199bdbab` |
|
||||
| Sidecar | fresh start from this tree — `WAGGLE_SKIP_LITELLM=1 tsx packages/server/src/local/start.ts` on `:3333`; `/health` → `database.healthy:true`, personal mind frameCount 3 |
|
||||
| New-route presence proof | `GET /api/memory/999999/trace` (no auth) → **401 MISSING_TOKEN** (route exists) — NOT a Fastify "route not found" 404, i.e. the live sidecar has the PR3.5 code |
|
||||
| Vite | running `:8080`, **serves this tree** (its served `MemoryTrustManage.tsx` module contains `ConfidenceRing` + `/confirm`) and proxies `/api` → my fresh `:3333` |
|
||||
| Auth | bearer from `GET /api/auth/session-token` (loopback, no Origin needed) |
|
||||
|
||||
---
|
||||
|
||||
## 1A — `GET /api/workspaces/:id/context` projects `frame.source` ✅
|
||||
|
||||
The provenance ⬡ pill on the Workspace "What Waggle knows" rows is fed by the `source`
|
||||
column the SELECTs now carry (workspaces.ts:394 / :411).
|
||||
|
||||
| Workspace | recentMemories | sources observed |
|
||||
|---|---|---|
|
||||
| `writer-demo-anya` | 8 | **`user_stated` AND `tool_verified`** (multi-source — proves not hardcoded) |
|
||||
| `default-workspace` | 1 | `user_stated` |
|
||||
| `new-hive` | 0 | (honest empty) |
|
||||
|
||||
`recentDecisions[].source` projected too. Writer-demo frames 14/15 carry `agent_inferred`
|
||||
(the backend-review H-1 honesty fix — agent writes are no longer silently `user_stated`).
|
||||
|
||||
## 1.5 — `POST /api/memory/:id/confirm` + `GET /api/memory/:id/trace` ✅
|
||||
|
||||
**confirm** (HTTP + audit-trail proof):
|
||||
- `POST /api/memory/47/confirm` → `status: active`, id 47. (404 on `999999`.)
|
||||
- End-to-end write proof in `~/.waggle/audit.db`:
|
||||
`event_type=memory_write ws=personal 2026-06-16 15:43:56 in={"frameId":47,"action":"confirm"} out={...,"status":"active"}`
|
||||
→ route → `setMetadata` → `emitAuditEvent` → DB. Workspace-mind path also exercised
|
||||
(writer-demo-anya frame 12, `mind:workspace`).
|
||||
- The drawer for M-47 shows `updated 6/16/2026, 5:43:56 PM` — the exact `updatedAt` my
|
||||
confirm stamped (live persistence proof).
|
||||
|
||||
**trace** (all three paths):
|
||||
- `GET /api/memory/47/trace` → `{"trace":null}` (honest — manual frame, no `trace_id`).
|
||||
- `GET /api/memory/999999/trace` → 404 `Memory not found`.
|
||||
- `GET /api/memory/abc/trace` → 400 `Invalid memory id`.
|
||||
- Populated `{trace:{...}}` requires a chat-written frame carrying `metadata.trace_id`
|
||||
(pattern-write-back path) — none in the dev data; unit-covered (memory-center 25/25).
|
||||
|
||||
## UI smoke — Memory-Trust front door vs the FRESH sidecar ✅
|
||||
|
||||
`http://localhost:8080/memory` → **Trust tab is the default Memory view**.
|
||||
|
||||
- **Manage** (`01-manage-fresh-sidecar.png`): honest stat bar — `3` memories · **`—`**
|
||||
high-confidence (gated, not faked) · `0` stale · `0` awaiting confirm. 3 rows with the
|
||||
live 3-segment provenance line `⬡ M-47 · source: you · ● fresh`; M-1 shows real freshness
|
||||
`● aging — added 9w ago`. "Forgotten" filter honestly disabled (hard-delete, no recovery).
|
||||
- **Detail drawer**: `source: user_stated`, `updated …5:43:56 PM` (my confirm write).
|
||||
- **"Why is this here?"** → FE fired `GET /api/memory/47/trace?mind=personal` → **200** →
|
||||
Why view rendered the no-fabrication empty state: *"No trace is linked to M-47. This
|
||||
memory was added manually, imported, or written before traces were linked…"*
|
||||
(`02-why-trace-null-fresh-sidecar.png`).
|
||||
- All FE→backend calls 200 (`/api/memory?mind=personal&limit=500`, `/api/memory/stats?scope=all-minds`,
|
||||
`/api/workspaces`, `/api/memory/47/trace`). **Console: 0 errors, 0 warnings** (whole session).
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
- Audit log shows a **prior** live-verify ran at 15:18 (throwaway frames, since cleaned up)
|
||||
— consistent with concurrent activity earlier in the day; this pass independently
|
||||
reproduces PASS on a freshly-restarted sidecar.
|
||||
- No code changed in this pass — verification only. Evidence is uncommitted (founder's call).
|
||||
|
||||
## Next (ship)
|
||||
1. Mark **PR #17** ready. It's stacked on **PR #16** → merge/rebase **#16 → main first**,
|
||||
then **rebase #17 onto main**.
|
||||
2. Optionally commit this evidence dir alongside the prior `smoke-pr35-20260616/`.
|
||||
1898
docs/redesign-warm-hive/smoke-pr4-20260617-after-search.txt
Normal file
34
docs/redesign-warm-hive/smoke-pr4-20260617.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# PR4 live smoke — 2026-06-17
|
||||
|
||||
Branch `feature/warm-hive-pr4` @ `d9123060`. Sidecar (tsx, transpile-only → runs the new route/decoration from source; `WAGGLE_SKIP_LITELLM=1`) on :3333 vs the founder's real `~/.waggle` (tier TRIAL, Marketplace DB loaded). Vite on :8080. Chrome-devtools MCP.
|
||||
|
||||
## Backend route (curl, real marketplace.db + connector registry)
|
||||
`POST /api/marketplace/agent-search` with a minted session token:
|
||||
- `{"need":"send a message to my team in slack"}` → **7 candidates**, three-up:
|
||||
- **connector** `Slack` — why `"matches: send, message, slack"` — `install:{mode:"active"}` (Slack is connected in this vault → honestly shown as active, no install).
|
||||
- **skill** `slack-cursor` — `install:{mode:"store", extensionId:"pkg:84", type:"mcp", kind:"package", packageId:84}` (real id rejoin).
|
||||
- **tool** `microsoft-teams-mcp-server` — `pkg:194616`.
|
||||
- `{"need":""}` → **HTTP 400** (validation).
|
||||
- No-auth → **HTTP 401** (gated). `/health` → ok.
|
||||
|
||||
Confirms: the connector lane (registry keyword-match), the marketplace name→packageId rejoin, and the route wiring all work against live data.
|
||||
|
||||
## Frontend (chrome-devtools, /marketplace)
|
||||
- Surface renders: header **Marketplace** + **" installed" count bar** (D1, store-derived); **4 shelves** — `All` (pressed) / `Skills` / `Connectors` / `MCPs` (D2); search box; real marketplace packages (agent-skills, awesome-agent-skills, awesome-openclaw-skills, …).
|
||||
- **"Browse only"** label on marketplace-pack cards (review LOW #6 fix — no blank action slot).
|
||||
- Grid cards show **type-aware verbs** Add / Connect / Enable + honest "Available" badges (D3, §1 one-click).
|
||||
- **AskBar** ("Ask Waggle") + example chips render at the top of the browse tab.
|
||||
- Clicking the example chip "send a message to my team" → the **three-up suggestion box** rendered live:
|
||||
- **Connector** slot — why `"matches: send, message, team"` — **Connect** button.
|
||||
- **Skill** slot — why `"name matches: team; content mentions: send, message"` — **Enable** button.
|
||||
- **Tool** slot — **Enable** button.
|
||||
- **0 console errors / warnings** — on initial load AND after the agent-search (checked twice).
|
||||
|
||||
Evidence: `smoke-pr4-20260617-marketplace.png` (viewport), `smoke-pr4-20260617-after-search.txt` (a11y snapshot after search).
|
||||
|
||||
## Not exercised (deliberate)
|
||||
- Did **not** click Add/Connect/Enable on a real package/connector — that mutates the founder's real `~/.waggle` (installs a real package / writes a real vault credential). Cross-surface count-sync on install is component-tested (`pr4-install-store`, `phase4b` "count ticks up"); the inline chat card is covered by `pr4-inline-capability`.
|
||||
- The agent emitting `kind:'connector'/'mcp'` markers (so connector/mcp offers appear inline in chat) is a prompt-level follow-up; the parser + card support it.
|
||||
|
||||
## Gates at smoke time
|
||||
tsc apps/web 0 · tsc packages/server 0 · full FE vitest 1052/1052 · server helpers 12/12 · adversarial review HIGH+MEDIUM fixed + regression-locked.
|
||||
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
60
docs/redesign-warm-hive/smoke-pr5-20260617/REPORT.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Warm-Hive PR5 — Phase D Live Smoke (2026-06-17)
|
||||
|
||||
> **Verdict: PASS.** PR5 (Settings models-first/failover reskin + Onboarding 6-step + hard model gate) is live-verified end-to-end against a real sidecar. All five build gates green. **0 PR5-attributable console errors.** Ready for merge per the founder's call.
|
||||
|
||||
## Scope
|
||||
Phase D of `docs/redesign-warm-hive/PR5-BUILD-PLAN.md §5` — the final phase. The adversarial review already ran (it produced the `80e44a16` HIGH fix: invalid Google/Gemini key was reported "verified"). This is the remaining **live browser smoke**.
|
||||
|
||||
- **Branch:** `feature/warm-hive-pr5` · **HEAD:** `80e44a16`
|
||||
- **Stack:** Vite dev (`:8080`) → proxy → Fastify sidecar (`:3333`, `WAGGLE_SKIP_LITELLM=1`), driven via Chrome DevTools MCP.
|
||||
- **Two sidecar states used:** (1) real `~/.waggle` vault (12 provider keys + 7 Ollama models present) for the reskin + happy-path; (2) a throwaway **clean-vault** sidecar (`HOME=temp`, empty vault, `OLLAMA_HOST` → dead endpoint → `useHasWorkingModel=false`) to live-test the gate's *blocking* purpose. Clean sidecar verified zero models: `providers_with_key=[]`, `ollamaInstalled:false, totalLocalModels:0`.
|
||||
|
||||
## Verification gates (re-run fresh at HEAD `80e44a16`)
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `tsc -p apps/web/tsconfig.app.json` | **0 errors** |
|
||||
| `tsc -p packages/server/tsconfig.json` | **0 errors** |
|
||||
| FE PR5 vitest subset (model-gate, onboarding steps, tier-filter, settings-reskin) | **48/48 (8 files)** |
|
||||
| server `llm-key-probe.test.ts` (D3 live probe) | **16/16** |
|
||||
| eslint on PR5-changed TS/TSX | **0 errors** (22 benign "file ignored by pattern" warnings) |
|
||||
|
||||
## A. Settings → Models reskin (real vault) — PASS
|
||||
Route `/settings` (`SettingsRoute` → `SettingsApp`). Screenshots `01`, `02`.
|
||||
- **Models is the default/lead tab.** Rail = General · Models · **Plan** (Billing→Plan, **D8**) · Permissions · Team · Backup · **Enterprise** (kept separate, **D8**) · Advanced.
|
||||
- **"≥1 working model" banner** present & green: *"You have a working model — you're ready to go."*
|
||||
- **Shared `ModelGate` is the Models lead**: API-key tab (12 provider chips, all "key configured") + Local-model tab (*"Ollama detected — 7 models installed"* + pull-a-model input, **D12** synchronous pull) + BYO copy *"…stored encrypted in your Vault and never leaves your machine"* (**D1**).
|
||||
- **Top-right `Show: Essential / Standard / Everything`** segmented control (**D6**).
|
||||
- **D7 verified live:** at **Standard**, the **Advanced** *and* Enterprise tabs disappear from the rail (collapses to General·Models·Plan·Permissions·Team·Backup); at **Everything** they return. The same dial also gates the dock's "PINNED · POWER TOOLS" — confirming **D6**'s single global disclosure axis (not a second persistence key), as designed.
|
||||
- **Model Pilot failover chain preserved** (Primary→Fallback→Budget Saver + $/$$/$$$ + daily-budget) — not rebuilt.
|
||||
- **0 console errors.**
|
||||
|
||||
## B. Onboarding 6-step, completes with a model (real vault) — PASS
|
||||
`/?forceWizard=true`. Screenshots `03`, `04`, `05`.
|
||||
- **Re-key 5→6 nav is sound** (the plan's highest regression risk). Top progress bar spans 6 steps (`valuemax=5`, 0-indexed). Walked: **1 Welcome → 2 About-you → 3 Model gate → 4 Import → 5 Template → 6 First-task**, Back/Continue and progress dots tracked correctly.
|
||||
- **Step 3 mounts the same shared `ModelGate`** (API-key/Local tabs, provider chips, banner) **+ both D2 controls** ("I'll do this later" + "Continue"). With keys present the gate is open (Continue enabled).
|
||||
- **Step 5 Template = curated 6** (**D5**): Research Hub · Engineering · Sales Pipeline · Marketing & Content · Product Management · Blank Workspace.
|
||||
- Selecting **Research Hub** fired the real `POST /api/workspaces` → **201 Created**, mapped the **Researcher** persona (TEMPLATE_PERSONA), and advanced to First-task with the task box **pre-seeded from the template hint** ("Help me design a literature review on my topic").
|
||||
- **"Let's go!" landed inside the new workspace** at `/workspaces/research-hub/chat` — Researcher persona, workspace-scoped memory/skills, first task seeded into the composer (not auto-sent — correct, no surprise LLM call).
|
||||
- **Inner step counter** reads "Step N of 4" on the 4 middle config steps (Welcome=intro, First-task=finale have no counter). Verified intentional framing, *not* an off-by-one — but flagged for designer confirmation vs the 6-dot progress bar.
|
||||
|
||||
### Note — one non-PR5 console 409 (documented, not a blocker)
|
||||
On completing onboarding, `POST /api/tier/start-trial` returned **409** → console error. Response body: `{"error":"TRIAL_ALREADY_STARTED", "trialStartedAt":"2026-06-11", "trialDaysRemaining":9}`. This install already started its trial, so the duplicate is correctly rejected. **Not a PR5 regression** (trial-start is pre-existing onboarding-complete logic untouched by PR5; returns 200 on a genuinely fresh install). Workspace creation (201) and `onboarding/complete` (200) both succeeded. *Pre-existing nit (out of PR5 scope):* the FE logs this expected idempotency-409 as `console.error` instead of treating `TRIAL_ALREADY_STARTED` as a no-op.
|
||||
|
||||
## C. Clean-vault gate BLOCK + invalid-key honesty — PASS
|
||||
Clean-vault sidecar (zero working models). Screenshots `06`, `07`, `08`.
|
||||
- **Hard gate blocks (D2):** at step 3 with no model, banner flips to *"No working model yet — add a provider key or a local model below."* and **Continue is `disabled`** (tooltip "Add a working model to continue"). Provider chips show no "key configured" state.
|
||||
- **Invalid-key honesty (validates `80e44a16`):** selected Anthropic, pasted a syntactically-valid but bogus `sk-ant-…` key, clicked **Validate & save** → server-side live probe (`POST /api/settings/test-key` → `api.anthropic.com` 1-token ping) → alert **"Key was rejected by the provider."** Banner stays negative, **Continue stays disabled** — no fabricated "verified", bad key does not unblock the gate. (The 401 is server-side; never reaches the browser → no console error.)
|
||||
- **"I'll do this later" escape (D2):** dismissed onboarding → `/home` with the persistent safety-net banner **"No model yet. Add a provider key or a local model so your agent can actually run."** + **"Set up a model"** CTA (the C1 Home safety net). Home greeted "Welcome, Tester" (name persisted from the clean-vault identity write).
|
||||
- **0 console errors** across all three states.
|
||||
- *Minor cosmetic (pre-existing, not PR5):* the header model pill still renders the config `DEFAULT_MODEL` string `claude-sonnet-4-6` even with no key; the "No model yet" banner is the authoritative signal.
|
||||
|
||||
## Screenshots
|
||||
`01` settings-models-reskin · `02` settings-localmodel + Standard gating · `03` onboarding model-gate (open) · `04` onboarding first-task (seeded) · `05` landed in Research Hub workspace · `06` gate blocked (no model, Continue disabled) · `07` invalid key rejected · `08` Home "No model yet" banner.
|
||||
|
||||
## Decisions confirmed live
|
||||
D1 BYO-key · D2 hard gate + "later"→Home+banner · D3 live key-validate (honest reject) · D4 6-step shape · D5 curated-6 templates + persona map · D6 single Show dial · D7 Advanced→Everything-only · D8 Billing→Plan + Enterprise separate · D9/D10/D11/D12 as planned.
|
||||
|
||||
## Open / follow-ups (none block merge)
|
||||
1. FE swallow the expected `TRIAL_ALREADY_STARTED` 409 (pre-existing, out of PR5 scope).
|
||||
2. Designer confirm "Step N of 4" inner counter vs the 6-step progress bar (intentional framing).
|
||||
3. Header model pill shows config default even with no key (pre-existing cosmetic).
|
||||
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 334 KiB |
BIN
docs/redesign-warm-hive/smoke-pr6a-20260618/04-notfound-404.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/redesign-warm-hive/smoke-pr6a-20260618/05-platform-boot.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 4.4 MiB |
44
docs/redesign-warm-hive/smoke-pr6a-20260618/REPORT.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Warm-Hive PR6a — Build + Review + Live Smoke (2026-06-18)
|
||||
|
||||
> **Verdict: PASS.** PR6a (Benchmarks 17 + Platform 18 + NotFound reskin) is built, reviewed, and live-verified against a real sidecar. All gates green. Review found **0 HIGH, 2 MEDIUM (both fixed + re-verified), 0 LOW.** **0 PR6a-attributable console errors.** Ready for merge into the PR6 line.
|
||||
|
||||
## Scope (PR6 sub-PR a — first of the 3-way split)
|
||||
Two net-new, pure-UI, static-data, **⌘K-only** surfaces + the 404 reskin. No backend. Branch `feature/warm-hive-pr6`.
|
||||
- **17 Benchmarks** — capabilities matrix (11 rows × 6 cols) + Memory SOTA (LoCoMo bars). `BenchmarkApp.tsx` (+test).
|
||||
- **18 Platform** — Desktop / Boot (static showcase) / Coming-next tabs. `PlatformApp.tsx` (+test).
|
||||
- **404** — `pages/NotFound.tsx` reskin (D5; standalone route, not a Platform tab).
|
||||
- **Wiring** (integrator-owned): `routes/{BenchmarkRoute,PlatformRoute}.tsx` + barrel, `App.tsx` (2 routes), `command-catalog.ts` (2 Power-tools ⌘K entries).
|
||||
|
||||
## Build method
|
||||
2 parallel build agents on disjoint files (Benchmark vs Platform/NotFound) → main-agent wiring of the shared files (no parallel-write conflicts). Decisions D3/D5/D6/D21 applied.
|
||||
|
||||
## Gates (re-run after the MEDIUM fixes)
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `tsc -p apps/web/tsconfig.app.json` | **0 errors** |
|
||||
| FE vitest (BenchmarkApp · PlatformApp · p7-b3-command-center) | **11/11 (3 files)** |
|
||||
| eslint (PR6a changed files) | **0 errors** |
|
||||
|
||||
## Adversarial review (4 dimensions, workflow `wf_2ce6b417`)
|
||||
- **Design-fidelity + no-fabrication (Benchmark):** **PASS** — all 11 matrix rows + 66 marks match `benchmark.html` exactly; LoCoMo bars 87.66/81.95/78.05/62.47 + stats (+5.71, p<10⁻⁵, 92.75%, 100% 58–83ms) verbatim; competitors honestly credited ● on deep terminal coding; "positioning view, not a lab benchmark" disclaimer + dated provenance present. No invented/rounded numbers.
|
||||
- **Correctness / a11y / routing:** **PASS** — wrappers above catch-all; unique ⌘K ids + real routes + valid icons; tablists use role/aria-selected; no runtime/console hazards.
|
||||
- **Warm-token (D21):** **PASS** — only warm semantic vars; the sole raw hex (`#1a1407` on-honey text, macOS traffic-light dots) are allowed OS-chrome exceptions per the design.
|
||||
- **Design-fidelity (Platform):** **2 MEDIUM → fixed:**
|
||||
1. Channel icon tints were hardcoded honey-wash → now per-channel per `platform.html:160-175` (Desktop honey · Browser **work-wash** · Messaging honey · Mobile **intel-wash**). Live-verified computed bg: honey `rgba(233,165,44,.1)` / work `rgba(122,166,214,.12)` / intel `rgba(177,150,221,.12)`.
|
||||
2. Boot hex used Tailwind `animate-pulse` (opacity) + static shadow → now a `boot-hex-pulse` keyframe (added to `index.css`, matching the repo's keyframe convention) animating the box-shadow per `platform.html:57-58`, **with a `prefers-reduced-motion` fallback**. Live-verified `animationName: boot-hex-pulse`, 2.2s.
|
||||
|
||||
## Live smoke (real sidecar :3333, Vite :8080)
|
||||
- **/benchmarks** — Capabilities matrix (11 rows, ●/◐/○, honey Waggle column, honest terminal-coding row, disclaimer) + Memory SOTA (4 bars + 3 stat chips + caveat + OSS link). Toggle works. **0 console errors.** (`01`, `06`)
|
||||
- **/platform** — 3 tabs: Desktop (macOS↔Windows toggle + spec tiles), Boot (static showcase, pulsing hex), Coming-next (4 channel cards, per-channel tints). **0 console errors.** (`02`, `03`, `05`)
|
||||
- **404** — `/this-cell-is-empty` → reskinned "This cell of the hive is empty." + back-to-Home + ⌘K hint. The one console.error is the **intentional 404 telemetry log** (pre-existing `NotFound` behavior), fires only on a real bad route. (`04`)
|
||||
- **⌘K discovery** — typing "benchmark" surfaces the live "Benchmarks — capabilities · memory SOTA" entry (DOM-confirmed; cmdk items aren't exposed in the a11y tree).
|
||||
|
||||
## Decisions honored
|
||||
D3 (inline static benchmark data, dated 2026-06-18, verbatim, no fabrication) · D5 (404 = standalone NotFound route, not a Platform tab) · D6 (Boot = static showcase, not wired to real AppShell boot) · D21 (warm tokens; OS-chrome hex exempt).
|
||||
|
||||
## Screenshots
|
||||
`01` benchmark SOTA · `02` platform desktop · `03` platform coming-next (per-channel tints) · `04` 404 reskin · `05` platform boot (pulse) · `06` benchmark capabilities matrix.
|
||||
(Committed: 02–05 small; 01/06 are large full-page captures kept local.)
|
||||
|
||||
## Open / follow-ups (none block merge)
|
||||
- None for PR6a. Next in the PR6 line: **PR6b** (reskin + feature-fill of 06·07·08·12·16) then **PR6c** (04·15).
|
||||
BIN
docs/redesign-warm-hive/smoke-pr6b-20260618/01-launcher-varB.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.8 MiB |
BIN
docs/redesign-warm-hive/smoke-pr6b-20260618/04-usage-budget.png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
BIN
docs/redesign-warm-hive/smoke-pr6b-20260618/06-profile-knows.png
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
45
docs/redesign-warm-hive/smoke-pr6b-20260618/REPORT.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Warm-Hive PR6b — Build + Review + Live Smoke (2026-06-18)
|
||||
|
||||
> **Verdict: PASS.** PR6b reskins + feature-fills 5 shipped apps (Launcher · Storage/Files · Power surfaces · Evolution · App-sextet) in place. Built by 5 parallel agents, adversarially reviewed, and live-smoked. Review: **0 real HIGH (the 7 "HIGH" were PASS confirmations of the no-fabrication/regression contract), 1 actionable design-fidelity fix**, both that + a live-smoke finding fixed + re-verified. Gates green. **0 PR6b-attributable console errors.** Ready for commit.
|
||||
|
||||
## Scope (PR6 sub-PR b — the bulk; reskin-in-place, no rebuild)
|
||||
| Screen | Change | Files |
|
||||
|---|---|---|
|
||||
| **B1 Launcher (06)** | Variation B (memory-sharing explainer) behind an A/B toggle; Variation A unchanged | LauncherApp.tsx (+test) |
|
||||
| **B2 Storage (07)** | Variation A ("Where it lives": on-disk tree + storage-type cards) via StorageAndFilesApp A/B wrapper; FilesApp **Source column** (D11) | StorageApp, StorageAndFilesApp (new, +test), FilesApp, FilesRoute |
|
||||
| **B3 Power (08)** | Shared warm primitives + per-model spend bars + daily-budget (Usage), Vault Renew, color-stratified risk badges (Approvals). **D8: no route consolidation** | power/power-primitives (new, +test), TelemetryApp, VaultApp, ApprovalsApp, AutomationCenterApp |
|
||||
| **B4 Evolution (12)** | "By skill" version-ladder over real run data; D13-gated provenance badge | memory/EvolutionTab.tsx (+test) |
|
||||
| **B5 Sextet (16)** | Room two-col, Artifacts 3-col grid (per-kind tints), Profile "what Waggle knows" (D20), Agents swarm CTA; **Mission Control = CockpitApp** warm sweep | RoomApp (+test), ArtifactCenterApp, UserProfileApp (+test), AgentsApp, CockpitApp |
|
||||
|
||||
## Build method
|
||||
5 parallel build agents on disjoint file-sets (surgical/preserve-behavior + no-fabrication mandates); main-agent integration of the shared sweep. The concurrent agents shared one working tree → transient cross-agent tsc "errors" during the run resolved clean at the end.
|
||||
|
||||
## Gates (after fixes)
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `tsc -p apps/web/tsconfig.app.json` | **0 errors** |
|
||||
| FE vitest (full) | **1130 passing** (2 PR5 settings tests are **load-flaky** — pass 3/3 in isolation in ~1s; they render the heavy SettingsApp and time out only under full-suite concurrency. Not PR6b-related.) |
|
||||
| eslint (changed files) | **0 errors** |
|
||||
|
||||
## No-fabrication — all six traps defused at build time (review-confirmed)
|
||||
- **B1** provenance chip prefixed **"example ·"** (not a real recall).
|
||||
- **B2 (D11)** Source column = **"—"** — grounded: `FileEntry` (core schema) carries no creator field anywhere. memoryCount gated to "—" when absent (no fake "142 memories").
|
||||
- **B3 (D9/D13-class)** per-model bars from real `/api/cost/summary.byModel` (absent → gated); budget from `/api/settings`; Vault "Renew" reuses the real upsert (no rotate endpoint exists — not invented); Approvals risk **derived** from tool name (no risk field in payload, not fabricated).
|
||||
- **B4 (D13)** absolute quality % gated off (only `delta_accuracy` exists → honest +N.Npp); "GEPA · judged by 3 models" **never rendered** (no judge roster stored).
|
||||
- **B5 (D20)** Profile facts only from real stored fields (empty → section hidden); Room participants from live SSE only (design's fake "Jonas P · invited" **omitted**); Artifact source = coarse real class (not invented "made by Claude Code").
|
||||
|
||||
## Two findings fixed (review + live smoke each caught one the other missed)
|
||||
1. **Mission Control was edited on dead code (live smoke).** `/settings/mission-control` renders **`CockpitApp`** — `MissionControlApp.tsx` is explicitly KILLED (rendered nowhere). B5 had relabeled the dead component (invisible + a §3.3 dead-code edit). **Fix:** reverted the dead-file edit; applied the D21 warm-token sweep to the live `CockpitApp` (8 raw emerald/amber/green → `--healthy`/`--attention`). The "Services-lead tab relabel" doesn't apply — CockpitApp is a single-scroll dashboard, no fleet/team/activity tabs (recon mis-mapping, noted).
|
||||
2. **Artifact icons lost their per-kind palette (review).** B5 flattened all card icons to uniform honey-wash; design §16 uses per-kind tints. **Fix:** added a `KIND_TINT` map (9 kinds → warm `work`/`healthy`/`intel`/`honey` washes — design-faithful *and* D21-compliant) + applied. tsc + full vitest green after.
|
||||
|
||||
## Live smoke (real sidecar :3333, Vite :8080) — 0 console errors on every surface
|
||||
- **B1** `/launcher` — A/B toggle; Variation A (live detect/launch preserved) + Variation B (3-node flow + 3 cards + "example ·" provenance). (`01`)
|
||||
- **B2** `/files` — A/B toggle; "Where it lives" (real tree: palindrome.py 222 B, dirs "—") + Files browser (storage-type tabs preserved + **Source column "—"**). (`02`,`03`)
|
||||
- **B3** `/settings/usage` — Daily Budget editor + real Tool Usage breakdown; per-model bars gated off (no spend). (`04`)
|
||||
- **B4** `/memory` Evolution — "By skill / By run" toggle (existing run-review preserved). (`05`)
|
||||
- **B5** `/room` (empty state preserved, no fake participants); `/settings/profile` "What Waggle knows" facts from real data (`06`); `/artifacts` 3-col grid (data fetch timed out under sidecar load — env, not code; per-kind tints code-verified) (`07`); Mission Control = CockpitApp (warm).
|
||||
|
||||
## Open / not blocking
|
||||
- `/artifacts` data fetch timed out (10s) under sidecar load during the smoke — environment, not PR6b; the error state + Retry render correctly. Per-kind tints verified in code + tsc/vitest.
|
||||
- The 2 load-flaky PR5 settings tests (pass in isolation) — pre-existing harness flakiness, documented.
|
||||
- Next: PR6c (04 All-workspaces + 15 Habit).
|
||||
|
After Width: | Height: | Size: 4.2 MiB |
38
docs/redesign-warm-hive/smoke-pr6c-20260618/REPORT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Warm-Hive PR6c — All-workspaces (04) + Habit (15) — Build + Review + Live Smoke (2026-06-18)
|
||||
|
||||
> **Verdict: PASS.** C1 (AllWorkspacesApp) is built, reviewed (PASS, 2 LOW fixed), and live-verified. C2 (Habit streak) is verified correctly gated — no change is the honest outcome (no real streak field exists; flipping would fabricate). Gates green. **0 console errors.** Final sub-PR of the PR6 line.
|
||||
|
||||
## Scope (PR6 sub-PR c — the data surfaces)
|
||||
- **C1 — All workspaces (04):** new `AllWorkspacesApp` — the full workspace shelf (Home stays the day view; this is "every workspace"). Grid (D14, Table = disabled "soon" toggle), name search, storage-type filter pills, per-card `WorkspaceActionsMenu`, click→open, empty state (D16, reuses `CreateWorkspaceDialog`). Replaces the `/workspaces`→`/home` redirect (D15) + adds a ⌘K Jump entry.
|
||||
- **C2 — Habit (15):** backend-blocked — verification only (see below).
|
||||
|
||||
## C1 build + wiring
|
||||
- **New:** `apps/web/src/components/os/apps/AllWorkspacesApp.tsx` (+ test, 8/8), `apps/web/src/routes/WorkspacesRoute.tsx`.
|
||||
- **Wired (main):** routes barrel; `App.tsx` — replaced `<Route path="workspaces" element={<Navigate to="/home"/>}>` with `<WorkspacesRoute/>` (and dropped the now-unused `Navigate` import); `command-catalog.ts` — "All workspaces · every workspace · grid" Jump entry (LayoutGrid icon), ⌘K-only (D15).
|
||||
- Reuses `useShell()`'s single `useWorkspaces` instance (no duplicate fetch), `WorkspaceActionsMenu`, `CreateWorkspaceDialog`, warm primitives.
|
||||
|
||||
## Gates
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| `tsc -p apps/web/tsconfig.app.json` | **0 errors** |
|
||||
| FE vitest (AllWorkspacesApp + p7-b3-command-center) | **green** (AllWorkspacesApp 8/8) |
|
||||
| eslint (changed files) | **0 errors** |
|
||||
|
||||
## Adversarial review — PASS (0 HIGH/MEDIUM; 2 LOW fixed)
|
||||
- **No-fabrication: clean.** Every stat gated on a real optional `Workspace` field — `memoryCount`→`—` when absent (never a digit), `sessionCount`/`lastActive`/`storageType` badge omitted when absent. The design's "N to review" pending chip is correctly **omitted** (no `pendingCount` field exists — showing it would fabricate).
|
||||
- **Correctness / design-fidelity / warm-token / a11y: clean.**
|
||||
- **LOW-1 fixed (honesty):** the green dot was labeled "agent live" but derived from `health === 'healthy'` (no real per-workspace agent-activity field exists) → **relabeled to "healthy"** (honest meaning), variable renamed `isHealthy`.
|
||||
- **LOW-2 fixed (a11y):** filter pills container `role="group"` → **`role="radiogroup"`** for the `aria-checked` radios.
|
||||
|
||||
## Live smoke (real sidecar :3333) — 0 console errors
|
||||
- `/workspaces` **renders the shelf** (no longer redirects to Home): "Workspaces" heading + New-workspace, search, storage pills (All 4 / Virtual·Local·Team with live counts), "Table — soon" disabled (D14), 4 cards each with `WorkspaceActionsMenu` and **"—" for absent memoryCount (no fabrication)**. (`01`)
|
||||
- **Search** "Research" → narrows to "1 WORKSPACE" (Research Hub). **Open** → navigates to `/workspaces/research-hub`. **⌘K** "All workspaces" present in the Jump group (Home → All workspaces → Chat), routes to `/workspaces`.
|
||||
|
||||
## C2 — Habit streak (15): verified correctly gated, no change (D17)
|
||||
- `HomeCockpit.tsx`: `SHOW_STREAK = false`; the chip is `{SHOW_STREAK && <StreakChip/>}` with `TODO(backend): expose a real streak on HomeBriefing, then flip this to true`.
|
||||
- `HomeBriefing` (types.ts) carries **no `streak` field** — the dependency is genuinely absent.
|
||||
- Per D17, the honest state is to **leave it gated** (a hardcoded/invented streak violates the no-fabrication contract — the PR3 streak precedent). OvernightHero already correct. Depth/nudges deferred (D18). **No code change.**
|
||||
|
||||
## Open / not blocking
|
||||
- Minor systemic cosmetic: the header breadcrumb label for the 3 ⌘K-only PR6 routes (benchmarks/platform/workspaces) shows a fuzzy `matchNavRoute` fallback (e.g. "Chat" on /workspaces) — these routes aren't in the nav `labelEntries`. Out of scope to fix here (shared AppShell label logic affecting all routes); documented for a polish follow-up.
|
||||
- PR6 line is now complete (PR6a + PR6b + PR6c). Next per roadmap: PR7 (Auth/Billing), PR8 (landing).
|
||||
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
46
docs/redesign-warm-hive/smoke-pr7a-20260624/REPORT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# PR7a Billing — Live Smoke (2026-06-24)
|
||||
|
||||
Branch `feature/warm-hive-pr7a`. Live-driven via chrome-devtools against a freshly-started
|
||||
dev stack: sidecar **:3333** (`tsx src/local/start.ts`, TRIAL tier, **Stripe NOT configured** —
|
||||
no `STRIPE_SECRET_KEY`) + vite **:8080** (proxies `/api` → 3333). Dark + light. **0 console errors throughout.**
|
||||
|
||||
The Stripe-unconfigured dev env is the *ideal* condition to prove the F8 honest state end-to-end.
|
||||
|
||||
## Verdict
|
||||
**PASS.** Every adversarially-confirmed review fix verified against the running app. The smoke also
|
||||
**caught a real regression the unit tests could not**: finding #4's glow fix (a Tailwind class swap)
|
||||
still rendered `box-shadow: none` — a Tailwind v4 arbitrary-shadow gotcha. Fixed live to the proven
|
||||
inline-style pattern and re-verified.
|
||||
|
||||
## What was verified (file:evidence)
|
||||
|
||||
| Finding / behavior | Expected (honest) | Live result | ✓ |
|
||||
|---|---|---|---|
|
||||
| **D12** `/settings?tab=billing` deep-link | snaps to Plan tab | Plan tab `aria-selected=true` | ✅ |
|
||||
| **#7** §14 subhead | "Memory is free forever. You only pay for scale…" | renders (dark + light) | ✅ |
|
||||
| **#6** TRIAL "Current" | trial ≠ purchasable plan → Solo NOT "Current"/"Your plan" | tier=TRIAL → Solo shows **"Included"**, no "Current" badge anywhere | ✅ |
|
||||
| **#8** a11y | honest `aria-pressed` toggle buttons (not a false radiogroup) | Monthly/Annual are `button[aria-pressed]`; clicking flips pressed-state | ✅ |
|
||||
| **#3 / F8** Stripe-not-configured | upgrade CTAs disabled **pre-click**, not a 503 after | probe `GET /api/stripe/status` → `configured:false` → Pro/Teams CTAs **"Unavailable" disabled** | ✅ |
|
||||
| **#4** Pro "Most popular" glow | honey glow on the focal card | **was `none` even after class swap** → fixed to inline `boxShadow:var(--shadow-honey)` → renders `rgba(233,165,44,.25) 0 0 0 1px, rgba(233,165,44,.35) 0 8px 30px -10px` (dark + light) | ✅ (fixed in smoke) |
|
||||
| Annual toggle (D8/F9 display) | price swaps to the real annual labels | Monthly $19/$49 → Annual **$15 / mo · billed yearly**, **$39 / seat · yearly** | ✅ |
|
||||
| **F5** `/payment-success` no-fabrication | no completed checkout → honest, never a fake receipt | headline **"Nothing to confirm"**; no "You're Pro", **zero** receipt rows (no Trial-ends / Emailed / Receipt / $19.00 / VISA / ···4242) | ✅ |
|
||||
| Console | 0 errors/warnings | clean on `/settings?tab=billing` (dark+light) and `/payment-success` | ✅ |
|
||||
|
||||
## The regression the smoke caught (#4 glow)
|
||||
`shadow-[var(--shadow-honey)]` (Tailwind v4 arbitrary class) computed to `box-shadow: none` on the
|
||||
Pro card — verified twice incl. a hard reload (`ignoreCache`). The sibling `border-[var(--honey-line)]`
|
||||
from the *same* class string DID apply, so it's specific to the arbitrary-shadow-with-CSS-var utility,
|
||||
not staleness. Injecting `style.boxShadow='var(--shadow-honey)'` resolved to the full glow, so the fix
|
||||
is the **BenchmarkApp inline-style pattern** (`BenchmarkApp.tsx:367`). Re-verified: glow renders in
|
||||
dark and light. (AskBar/ChatApp use `focus-within:shadow-[var(--shadow-honey)]` — unverified here,
|
||||
flagged as possibly the same latent gotcha; out of PR7a scope.)
|
||||
|
||||
## Evidence
|
||||
- `01-billing-plans-dark-annual.png` — dark, Annual selected: 3 cards, Pro glow, F8 "Unavailable" CTAs.
|
||||
- `02-billing-plans-light.png` — light (warm paper `rgb(247,241,228)`): glow + tokens + F8 hold.
|
||||
|
||||
## Not exercised (no honest data source in this env)
|
||||
- **Paid Success ("You're Pro") + Manage Portal launchpad** — needs a PRO/TEAMS tier + a real Stripe
|
||||
customer; covered by unit tests (`pr7a-billing.test.tsx`: synced-tier confirmation, success-race guard).
|
||||
- **Enabled checkout CTAs / hosted-Checkout redirect** — needs `STRIPE_SECRET_KEY` in the sidecar env;
|
||||
the disabled F8 path is the honest state here and was the one under test.
|
||||
|
After Width: | Height: | Size: 807 KiB |
|
After Width: | Height: | Size: 859 KiB |
|
After Width: | Height: | Size: 881 KiB |
73
docs/redesign-warm-hive/smoke-pr7b-20260624/REPORT.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# PR7b Auth (screen 13) — Live Smoke (2026-06-24)
|
||||
|
||||
Branch `feature/warm-hive-pr7b`. Live-driven via chrome-devtools against the dev stack
|
||||
(sidecar :3333 + vite :8080). Clerk shared instance **elegant-camel-8** (`pk_test_…`).
|
||||
Dark + light. **0 console errors throughout.**
|
||||
|
||||
## Verdict
|
||||
**PASS.** Both phases verified against the running app.
|
||||
|
||||
## B1 — pre-shell route + accountless state (no Clerk SDK)
|
||||
| Behavior | Live result | ✓ |
|
||||
|---|---|---|
|
||||
| `/auth` renders OUTSIDE AppShell | no Primary nav present | ✅ |
|
||||
| Honest local-first state | "you're running fully local" + "you don't need this to start" | ✅ |
|
||||
| Brand panel verbatim trust copy | "An account is optional…" + "Your memory stays yours…" | ✅ |
|
||||
| No fabricated identity | no demo email, no fake SSO button (F1/F10) | ✅ |
|
||||
| Continue → /home | navigates into the fully-local app | ✅ (unit) |
|
||||
|
||||
Evidence: `01-auth-b1-accountless-dark.png`.
|
||||
|
||||
## B2 — optional Clerk (D2(b)), themed
|
||||
With `VITE_CLERK_PUBLISHABLE_KEY` present, the key-gate flips `/auth` from the accountless
|
||||
notice to the **real** prebuilt Clerk form, app-wide `WaggleClerkProvider` mounted.
|
||||
|
||||
| Behavior | Live result | ✓ |
|
||||
|---|---|---|
|
||||
| Key-gate (accountless → Clerk) | `clerkMounted=true`, accountless notice gone | ✅ |
|
||||
| Real prebuilt `<SignIn/>` (D13) | `.cl-rootBox` mounted; email field present | ✅ |
|
||||
| Social buttons = dashboard config (no fabrication) | **Google + GitHub** render; **Apple absent** (not enabled) — honest (F10) | ✅ |
|
||||
| Warm theming (dark) | Clerk primary button `rgb(233,165,44)` = honey `#e9a52c` | ✅ |
|
||||
| Warm theming (light, theme-reactive) | primary `rgb(181,125,18)` = light honey `#b57d12` (tracked `useTheme`) | ✅ |
|
||||
| Sign in ↔ Sign up toggle | own toggle switches; §2b honey local-first note shows on Sign up | ✅ |
|
||||
| Redirect after auth | `<SignIn forceRedirectUrl="/home">` | ✅ (unit + props) |
|
||||
| Enterprise = sales CTA, not live SAML | "Talk to sales → kvark.ai"; no SAML field (F11/D15) | ✅ |
|
||||
| Router-integrated provider | `routerPush/replace → useNavigate` (no flicker) | ✅ |
|
||||
|
||||
Evidence: `02-auth-b2-clerk-signin-dark.png`, `03-auth-b2-clerk-signin-light.png`.
|
||||
|
||||
## Honesty / no-fabrication
|
||||
- Social buttons render **only what the dashboard enables** (prebuilt `<SignIn/>`), so the
|
||||
Google+Apple→Google+GitHub design/instance mismatch resolves itself — enable Apple in the
|
||||
dashboard and it appears with zero code change. No invented provider.
|
||||
- No-key path (production builds without the key) → the accountless local-first state, never a
|
||||
fake identity. The local sidecar still authorizes with its device token; Clerk is NOT the
|
||||
local API authorizer (the `getToken()`→Bearer cloud-sync seam is left for a later opt-in).
|
||||
|
||||
## Not exercised (needs a real account / manual QA)
|
||||
- A full sign-in/sign-up round-trip (Google/GitHub OAuth redirect, email-code verify) — needs
|
||||
real credentials. The component renders + themes + redirects-on-success are wired; the live
|
||||
OAuth round-trip (esp. inside a Tauri WebView, D4) is the documented follow-up spike.
|
||||
- `<UserButton/>` / `useUser()` feeding the sidebar user row — deferred (BUILD-PLAN §9).
|
||||
|
||||
---
|
||||
|
||||
## Adversarial review + fixes (2026-06-24, commit 0e0dc48e)
|
||||
|
||||
4-dim review (correctness/Clerk · no-fabrication-of-identity · design-fidelity · security/local-first)
|
||||
→ per-finding verify → synthesis. **3 confirmed: 1 CRITICAL, 1 MEDIUM, 1 LOW** — all fixed.
|
||||
|
||||
| # | Sev | Issue | Fix | Verified |
|
||||
|---|-----|-------|-----|----------|
|
||||
| 1 | **CRITICAL** | A malformed/placeholder `VITE_CLERK_PUBLISHABLE_KEY` (incl. the `.env.example` placeholder) blanked the WHOLE app at boot — ClerkProvider throws synchronously in render, and `WaggleClerkProvider` sits above `AppErrorBoundary` (uncatchable). Unit tests missed it (they mock ClerkProvider); the first smoke used a real key. | Shape-validate the key in `lib/clerk.ts` (prefix + base64→host-ending-`$`), **inlined** (the monorepo resolves multiple `@clerk/shared` majors). `.env.example` placeholder blanked. | **Live, real ClerkProvider:** with `pk_test_REPLACE_ME` the app **boots fully** (sidebar, routes to /home, /auth = accountless) — no blank, no error boundary. Valid key restored → Clerk form back. |
|
||||
| 2 | MEDIUM | Brand-panel hide breakpoint was `lg` (1024px); design hides < 820px → split lost across 820–1023px. | `min-[820px]` arbitrary variant (AuthBrandPanel + AuthScreen). | tsc + design intent |
|
||||
| 3 | LOW | Wordmark `font-semibold`/18px vs design 700/19px. | `font-bold`/`text-[19px]`. | Live: weight 700, size 19px |
|
||||
|
||||
**No identity-fabrication issues** found by the review: sidebar stays "Account"/"W", social buttons
|
||||
are dashboard-driven (prebuilt), no live SAML form, no demo identity, and the device-token adapter
|
||||
path is untouched (Clerk is not the local API authorizer).
|
||||
|
||||
Post-fix gates: FE tsc 0 · pr7b-auth **9/9** (+3 key-shape-gate tests) · full FE **1156/1156** ·
|
||||
prod build green · live smoke valid **+ invalid** key, dark+light, **0 console errors**.
|
||||
|
||||
**PR7b is review-clean and ready to merge.**
|
||||
BIN
docs/redesign-warm-hive/smoke-pr8-20260624/01-landing-hero.png
Normal file
|
After Width: | Height: | Size: 585 KiB |
|
After Width: | Height: | Size: 197 KiB |
BIN
docs/redesign-warm-hive/smoke-pr8-20260624/03-pricing.png
Normal file
|
After Width: | Height: | Size: 417 KiB |
47
docs/redesign-warm-hive/smoke-pr8-20260624/REPORT.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# PR8 Live Smoke — Warm-Hive `apps/www` landing reskin (2026-06-24)
|
||||
|
||||
**Branch:** `feature/warm-hive-pr8` @ `e44c83e9` (+ this report)
|
||||
**Server:** `next dev -p 3100` (fresh restart on clean `.next`; loaded `apps/www/.env.local`, Clerk `pk_test_` dev key present)
|
||||
**Viewport:** 1440×900 (Chrome DevTools MCP)
|
||||
|
||||
## Result: PASS — warm-Hive identity rendered on all surfaces, 0 console errors
|
||||
|
||||
| Surface | URL | Console | Evidence |
|
||||
|---|---|---|---|
|
||||
| Landing (hero) | `/` | **0 errors** (1 expected Clerk dev-key *warning*) | `01-landing-hero.png` |
|
||||
| Clerk sign-in | `/sign-in` | **0 errors** (1 expected Clerk dev-key *warning*) | `02-signin-clerk-warm.png` |
|
||||
| Pricing | `/#pricing` | — | `03-pricing.png` |
|
||||
|
||||
> The only console message on each page is Clerk's benign "loaded with development keys" warning — expected with the `pk_test_` dev key; production uses live keys (EXTERNAL-DEP). **Zero errors.**
|
||||
|
||||
## What the screenshots confirm
|
||||
|
||||
**01 — Landing hero:** warm graphite background (was cool `#08090c`), honey `#e9a52c`
|
||||
highlight on "We'll be the AI." + the eyebrow + Download CTA + the hive-viz hexagon and
|
||||
its four provider nodes (claude·sonnet / gpt·5 / qwen·local / gemini·2.5); **Hanken Grotesk**
|
||||
display type; JetBrains Mono in the viz chrome (`~/.waggle/hive · live`, `local · signed · 42ms`,
|
||||
stat labels via the now-live `var(--mono)`); subtle warm honeycomb texture. No cool blue-grey.
|
||||
|
||||
**02 — Clerk sign-in:** the prebuilt `<SignIn/>` card is fully warm — warm graphite card +
|
||||
border, honey `#e9a52c` **Continue** button with near-black text, honey "Sign up" link,
|
||||
GitHub + Google social buttons (dashboard-driven; Apple off, matching the apps/web instance),
|
||||
warm body text. Confirms the `layout.tsx` Clerk `appearance` hex remap.
|
||||
|
||||
**03 — Pricing:** honey "PRICING" eyebrow + "Honest pricing" highlight; three warm tier cards
|
||||
(Solo $0 / **Pro $19/mo — honey-bordered "MOST POPULAR"** / Teams $49/seat/mo · 3-seat min);
|
||||
honey checkmarks + "save 17%" annual pill; warm CTAs. **Prices/copy byte-identical to pre-PR8.**
|
||||
|
||||
## Honest-stats held (no-fabrication contract §5)
|
||||
- Proof section still shows the **conservative** framing (67.8% "three rivals agree", 0.3-pt
|
||||
convergence, 87.5% trio-strict single-hop) — **not** upgraded to the 87.66 SOTA headline.
|
||||
- `proof.human_quote` still empty; tiers/prices unchanged; all section copy intact (verified
|
||||
live in the accessibility snapshot + `git diff` shows `messages/en.json` unchanged).
|
||||
|
||||
## Not exercised (deliberate)
|
||||
- Real OAuth round-trip (Google/GitHub redirect) + email-code verify — needs real creds (manual QA, carried from PR7b P1).
|
||||
- Real Stripe checkout charge — hosted flow; live keys are EXTERNAL-DEP.
|
||||
- Light theme — landing is intentionally dark-only (deferred, see PR8-BUILD-PLAN §8).
|
||||
|
||||
## Gates at smoke time
|
||||
`tsc --noEmit` 0 · `next build` green (14/14 pages) · `next lint` clean · `vitest` 10/10 ·
|
||||
comprehensive hex re-audit: **zero non-warm hex** under `apps/www/app`.
|
||||