133 lines
9.1 KiB
Markdown
133 lines
9.1 KiB
Markdown
# Path-to-9 execution · PHASE B — instant power feel (Pillar 2)
|
||
### Contract: docs/ux-refactor/path-to-9-2026-07-07.md v3 §Pillar 2 (5/5 panel-endorsed).
|
||
### Prereq: Phase A merged (motion tokens exist — use SPRING/DUR/STAGGER + --mo-* vars for
|
||
### ALL new motion; the text-color guard is live — new text uses tokens).
|
||
|
||
Recon facts (verified pre-spec): Home loads via `adapter.getHomeBriefing()` +
|
||
`getHomeOvernight()` in HomeCockpit L549-589, gated on `connecting` settle, NO
|
||
persistence. LoginBriefing has its own module-scope `fetchBriefingData` + prefetch
|
||
(Wave T) — a SECOND data source (the drift-bug class the contract kills). Chat send
|
||
already renders an optimistic turn (ChatApp L741 comment); briefing landing state
|
||
machine `nextBriefingLanding` lives in AppShell (Wave U); BootScreen has `ready` prop +
|
||
850ms floor (Wave U). Session caches exist for memory (memory-list-cache.ts) and the
|
||
workspace shelf (module-scope in AllWorkspacesApp).
|
||
|
||
Per-lane gate: related vitest green + eslint 0 errors + `node scripts/ux-gates/text-color-guard.mjs`
|
||
clean on touched files. NO benchmarks/**, NO packages/hive-mind-*/**. NO cross-lane files.
|
||
|
||
## Lane H — home cache-first paint + one briefing truth + double-catch-up collapse (Pillar 2.1–2.4)
|
||
Files: `apps/HomeCockpit.tsx`, `os/overlays/LoginBriefing.tsx`, `os/AppShell.tsx`,
|
||
`os/BootScreen.tsx`, NEW `lib/home-cache.ts`, NEW `lib/briefing-source.ts`, tests.
|
||
The heart of the pillar — one lane so the data-unification isn't split across owners.
|
||
|
||
1. **Disk-persisted cache-first paint** (`lib/home-cache.ts`): persist the last
|
||
successful Home payload (briefing + overnight + the recall highlights) to
|
||
localStorage (versioned key, schema-guarded parse, size-capped). On mount,
|
||
HomeCockpit renders the cached payload IMMEDIATELY (before `connecting` settles,
|
||
before the sidecar answers) marked stale-invisible (no visual difference), then
|
||
refreshes silently. Day-0 (no cache) keeps today's skeleton path.
|
||
2. **Silent-refresh reconciliation CONTRACT** (v3 P2.3 — this is a TESTED contract):
|
||
when fresh data lands over a cached paint — (a) NO above-the-fold layout shift:
|
||
fixed-slot hero grammar (the Wave T card contract, applied to the hero) so text
|
||
swaps in place; (b) material deltas (counts, new items) animate via a small
|
||
honey delta pulse (use SPRING.micro/DUR.fast from Phase A — this is Pillar 3.3's
|
||
pulse, born here); (c) unit-test: cached render → fresh data with changed counts →
|
||
assert no element above the fold unmounts/remounts (key stability) and the
|
||
changed count carries the pulse class.
|
||
3. **ONE briefing truth** (`lib/briefing-source.ts`): extract LoginBriefing's
|
||
fetch+shape (fetchBriefingData/prefetch, highlights, workspace summaries,
|
||
brag counts) into this module. BOTH the home hero and the modal consume IT —
|
||
one fetch, one filter set, one count. Delete the second source. The
|
||
number-drift bug class dies here (regression-test: hero count === modal count
|
||
from the same mock).
|
||
4. **Double catch-up collapse** (v3 P2.4): the "I REMEMBER" recall cards render
|
||
INSIDE the home hero as its first staggered entrance (STAGGER.brief from
|
||
Phase A tokens); the modal fires ONLY on ≥7-day absences (N=7 per contract —
|
||
compute from the same lastActive the greeting uses), reusing the SAME card
|
||
component + briefing-source data. Update `nextBriefingLanding` gating in
|
||
AppShell accordingly (armed additionally requires awayDays ≥ 7). Tests: <7d →
|
||
no modal, cards in hero; ≥7d → modal (same data object as hero).
|
||
5. **Warm boot floor removal**: with cache-first paint, the BootScreen floor for
|
||
warm sessions drops — boot shows only until the shell mounts (brand flash
|
||
≤500ms; keep the full choreography for COLD/day-0 sessions where there is
|
||
nothing to paint). Keep Wave U's `ready` semantics for the cold path.
|
||
|
||
## Lane C — input-during-warmup + send-path budget + chat route-cache (Pillar 2.2, 2.5, 2.6-chat)
|
||
Files: `apps/ChatApp.tsx`, the chat state hook it uses (grep `useChat`/chat widget
|
||
state — verify real file), `os/WorkspaceDesktopApp.tsx` (entry interactivity only), tests.
|
||
1. **Composer accepts typing at paint**: the composer input must be enabled the
|
||
moment it renders — never disabled behind `connecting`/history-load. If a send
|
||
fires before the sidecar is ready: QUEUE it (one-deep queue is fine) with a
|
||
truthful per-message "waiting for connection…" state on the optimistic turn,
|
||
dispatch on ready, NEVER error or silently drop. Test: type+send while mocked
|
||
adapter is pending → message queued → adapter resolves → dispatched.
|
||
2. **Composer never locks on send**: verify (and lock with a test) that after send
|
||
the input clears and accepts the next message immediately while the previous
|
||
streams — no disabled window. If a lock exists, remove it.
|
||
3. **No dead clicks on cached surfaces**: WorkspaceDesktopApp entry — clicking a
|
||
tab/affordance during the entry skeleton either acts or shows a per-element
|
||
pending affordance (aria-busy + subtle shimmer), never nothing. Audit the entry
|
||
skeleton's click handling; fix silent swallows.
|
||
4. **Chat thread session cache**: mirror memory-list-cache — thread messages keyed
|
||
by (workspace, session) survive tab-away/return within the session; returning
|
||
renders instantly then refreshes silently. No re-skeleton on return.
|
||
|
||
## Lane K — keyboard-power layer (Pillar 2.7; double-yield with a11y)
|
||
Files: `os/WorkspaceActionsMenu.tsx` + `os/ContextMenu.tsx` (focus-reveal parity),
|
||
`apps/memory/MemoryTrustManage.tsx` (row-action focus parity ONLY — rest is Lane H/C
|
||
territory... verify no overlap; MemoryTrustManage is NOT touched by H/C in this phase),
|
||
`os/overlays/KeyboardShortcutsHelp.tsx`, `hooks/useKeyboardShortcuts.ts`, tests.
|
||
1. **Focus-reveal parity**: workspace-card kebab + memory-row actions (✓/✎/🗑)
|
||
reveal on :focus-within with visible focus-ring (the Phase-A `--focus-ring`
|
||
token), matching the chat action-row pattern (Wave T). Tab-through must reach
|
||
every action without a mouse.
|
||
2. **Hit areas**: kebab, memory row actions, modal ×, "Fix it now" → ≥40px
|
||
effective hit target (padding/pseudo-element expansion, no visual size change
|
||
needed). List each touched target + before/after size in the report.
|
||
3. **≤2-keystroke paths**: global shortcuts — new chat (existing? verify in
|
||
useKeyboardShortcuts; add if missing) and "open last workspace"; both reachable
|
||
from anywhere, documented in KeyboardShortcutsHelp.
|
||
4. **Shortcut cheat sheet**: verify KeyboardShortcutsHelp opens on `?` (and Cmd-/);
|
||
add the new shortcuts; ensure it lists the focus-reveal patterns ("Tab reaches
|
||
card actions").
|
||
|
||
## Lane R — route-cache: marketplace + agents (Pillar 2.6)
|
||
Files: `apps/MarketplaceApp.tsx`, `apps/AgentsApp.tsx`, NEW `lib/surface-cache.ts`
|
||
(generalize the memory-list-cache pattern: keyed session cache + test reset), tests.
|
||
1. Extract the proven pattern into `lib/surface-cache.ts` (typed, keyed, module-
|
||
scope, `resetForTests`). Do NOT migrate memory/workspaces onto it in this phase
|
||
(churn without yield — note as follow-up).
|
||
2. Marketplace: extensions list + facet state seed from cache on return; silent
|
||
refresh; no re-skeleton within a session.
|
||
3. Agents: roster + suggested cards seed from cache on return; same contract.
|
||
|
||
## Lane G2 — the hard interaction gate (v3 §3 verification)
|
||
NEW files: `scripts/ux-gates/warm-interaction-gate.mjs`, README update; root
|
||
package.json script `ux:warm-gate`. Playwright, mirrors capture-kit conventions
|
||
(seeded returning-user localStorage; disclose in output).
|
||
1. **Warm gate**: measure app-start → (a) home content visible, (b) composer
|
||
accepts a keystroke (type into it, assert value). FAIL if content >1000ms or
|
||
brand flash >500ms or first keystroke rejected. Print a timing table.
|
||
2. **Cold-start variant**: with the sidecar port BLOCKED (bad base URL env or
|
||
route-abort), assert cached paint still renders content + typing queues (needs
|
||
Lane H+C landed — the script probes, and reports which contracts hold; exits
|
||
1 only on regressions of landed contracts, with a --strict flag for full
|
||
enforcement once H+C merge).
|
||
|
||
## VERIFY STAGE (adversarial, after lanes)
|
||
- V1-instant: run `node scripts/ux-gates/warm-interaction-gate.mjs` against the dev
|
||
server (servers: vite 8080; sidecar recipe in the handoff) — report the ACTUAL
|
||
timing table; kill the sidecar and run the cold variant; try to refute Lane H/C
|
||
claims (type during warmup, click during skeletons, tab-away/return every cached
|
||
surface).
|
||
- V2-truth: refute the one-briefing-truth claim — grep for any remaining second
|
||
fetch path; run the reconciliation tests; force a count change through the mock
|
||
and check for above-fold remount; verify modal-vs-hero counts share one source.
|
||
- V3-keyboard: keyboard-only pass — Tab through home → workspaces → kebab →
|
||
memory rows → chat send; confirm every action reachable, focus-ring visible
|
||
(Phase-A token), no trap; report the actual traversal.
|
||
|
||
## Orchestrator after verify
|
||
tsc web + server, full apps/web vitest, ux-gates all green, browser smoke, commit.
|
||
Then: kit v6 build + R20 judge round (gate: min ≥ 8.0 per v3 §3) BEFORE Phase C.
|