mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* feat(server): add workspace session read APIs Expose workspace-scoped session list, detail, message, and snapshot reads so the client can fetch session data without depending on activation choreography. * feat(app): route mounted session reads through OpenWork APIs Use the new workspace-scoped session read endpoints for mounted OpenWork clients so the current frontend stops depending on direct session proxy reads for list, detail, message, and todo loading. * feat(app): add React read-only session transcript Introduce a feature-gated React island for the session transcript so we can replace the session surface incrementally while keeping the Solid shell intact. * feat(app): add React session composer surface Extend the feature-gated React session island to own its draft, prompt send, stop flow, and snapshot polling so the session body can evolve independently from the Solid composer. * feat(app): add React session transition model Keep the React session surface stable during session switches by tracking rendered vs intended session state and exposing a developer debug panel for render-source and transition inspection. * docs(prd): add React migration plan to repo Copy the incremental React adoption PRD into the OpenWork repo so the migration plan lives next to the implementation and PR branch. * docs(prd): sync full React migration plan Replace the shortened repo copy with the full incremental React adoption PRD so the implementation branch and product plan stay in sync. * feat(desktop): add React session launch modes Add dedicated Tauri dev and debug-build entrypoints for the React session path and honor a build-time React session flag before local storage so the alternate shell is easy to launch and reproduce. * fix(app): fall back to legacy mounted session reads Keep the new app working against older OpenWork servers by falling back to the original mounted OpenCode session reads when the workspace-scoped session read APIs are unavailable.
621 lines
27 KiB
Markdown
621 lines
27 KiB
Markdown
# PRD: Incremental React Adoption with Isolated Testing
|
|
|
|
## Status: Draft
|
|
## Date: 2026-04-05
|
|
|
|
## Problem
|
|
|
|
The OpenWork app is 100% SolidJS. The session UI has resilience issues (white screens, flicker, route/runtime/selection mismatches) rooted in overlapping owners of truth. The plan is to incrementally adopt React for the session experience layer, then expand to replace the entire app — while keeping the existing app running at every step. Each phase must be testable in isolation against a real Docker dev stack before merging.
|
|
|
|
## Current Architecture (Ground Truth)
|
|
|
|
### Frontend
|
|
- **Framework**: SolidJS only. Zero React in `apps/app/`.
|
|
- **Monolith**: `app.tsx` (~2,500 lines) creates ~15 stores, threads ~90 props to `SessionView`.
|
|
- **Session view**: `pages/session.tsx` (~2,000 lines) — SolidJS, receives all state as props via `SessionViewProps`.
|
|
- **State**: SolidJS signals + `createStore()`. No external state libs.
|
|
- **Router**: `@solidjs/router`, imperative navigation.
|
|
- **Prepared seam**: `@openwork/ui` already exports both React and Solid components. `SessionViewProps` is a clean data-only interface.
|
|
- **Build**: Vite + `vite-plugin-solid`. No React plugin configured.
|
|
- **Platform**: Tauri 2.x for desktop/mobile. Web mode uses standard browser APIs. Platform abstraction lives in `context/platform.tsx`.
|
|
|
|
### Backend
|
|
- **Server**: `Bun.serve()`, hand-rolled router. No framework.
|
|
- **Session data**: Lives in OpenCode's SQLite DB. Client reads via OpenCode SDK or proxied through `/w/:id/opencode/*`.
|
|
- **No server-side session read endpoints**: The OpenWork server has no `GET /sessions` or `GET /session/:id`. It proxies to OpenCode.
|
|
- **Activation**: Nearly free (array reorder). The expensive part is client-side workspace bootstrapping.
|
|
- **Orchestrator**: Process supervisor that spawns server + OpenCode + router.
|
|
|
|
### Styling
|
|
- **CSS framework**: Tailwind CSS v4.1.18 via `@tailwindcss/vite`.
|
|
- **Color system**: Radix UI Colors (30+ scales, 12 steps each) + DLS semantic tokens — all CSS custom properties (~700+).
|
|
- **Dark mode**: `data-theme` attribute on `<html>` + CSS variable swap. NOT Tailwind `dark:` prefix.
|
|
- **Component styling**: Inline Tailwind `class=` strings with template literal conditionals. No `cn()`, `clsx`, or `tailwind-merge`.
|
|
- **Custom CSS classes**: `ow-*` prefixed classes in global `index.css` (buttons, cards, pills, inputs).
|
|
- **CSS-in-JS**: None.
|
|
- **Animation**: CSS-only (Tailwind transitions + custom `@keyframes`). No framer-motion or JS animation libs.
|
|
- **Fonts**: System font stack (IBM Plex Sans preferred, no bundled fonts).
|
|
- **Design language**: `DESIGN-LANGUAGE.md` (871 lines) — quiet, premium, flat-first. Shadow is last resort.
|
|
- **Key files**: `tailwind.config.ts`, `src/app/index.css`, `src/styles/colors.css`, `DESIGN-LANGUAGE.md`.
|
|
|
|
### Existing domain map (CUPID)
|
|
The app follows CUPID domain organization:
|
|
- `shell` — routing, layout, boot, global chrome
|
|
- `session` — task/session experience, composer, messages
|
|
- `workspace` — workspace lifecycle, switching, connect
|
|
- `connections` — providers, MCP
|
|
- `automations` — scheduled jobs
|
|
- `cloud` — hosted workers, den
|
|
- `app-settings` — preferences, themes
|
|
- `kernel` — tiny shared primitives
|
|
|
|
---
|
|
|
|
## Three-Stage Transition: Solid → Hybrid → React
|
|
|
|
### Stage 1: React Island (Phases 0-3)
|
|
|
|
React lives inside the Solid app as a guest. Solid owns the shell, routing, and platform layer. React renders into a div that Solid manages.
|
|
|
|
```
|
|
Tauri/Web shell
|
|
└── Solid app (owns everything)
|
|
├── Solid sidebar
|
|
├── Solid settings
|
|
└── ReactIsland (a div)
|
|
└── React session view (our new code)
|
|
```
|
|
|
|
State bridge: minimal. React gets workspace URL + token + session ID from Solid via island props. React fetches its own data. Two independent state worlds.
|
|
|
|
### Stage 2: React Expands, Island Inverts (Phases 5-8)
|
|
|
|
React takes over more surfaces. Each Solid surface migrates to its React counterpart, one domain at a time. At a tipping point (after workspace sidebar moves to React), the island inverts:
|
|
|
|
```
|
|
Tauri/Web shell
|
|
└── React app (owns the shell now)
|
|
├── React sidebar
|
|
├── React session view
|
|
├── React settings (partial)
|
|
└── SolidIsland (a div) ← for remaining Solid surfaces
|
|
└── remaining Solid components
|
|
```
|
|
|
|
### Stage 3: React Owns Everything (Phase 9+)
|
|
|
|
```
|
|
Tauri shell (just the native window + IPC)
|
|
└── React app
|
|
├── react/shell/
|
|
├── react/session/
|
|
├── react/workspace/
|
|
├── react/connections/
|
|
├── react/app-settings/
|
|
├── react/cloud/
|
|
└── react/kernel/
|
|
```
|
|
|
|
At this point `vite-plugin-solid` and `solid-js` are removed. The app is a standard React SPA that happens to run inside Tauri for desktop. The web build is the same React app without the Tauri wrapper.
|
|
|
|
---
|
|
|
|
## State Ownership Rule
|
|
|
|
**At any point in time, each piece of state has exactly one framework owning it.**
|
|
|
|
When you migrate a surface from Solid to React, you delete the Solid version of that state. You never have both frameworks managing the same concern.
|
|
|
|
| Concern | Stage 1 (React island) | Stage 2 (React expanding) | Stage 3 (React owns all) |
|
|
|---------|----------------------|--------------------------|-------------------------|
|
|
| Session messages | React (react-query) | React | React |
|
|
| Session transition | React (transition-controller) | React | React |
|
|
| Workspace list | Solid | React (after migration) | React |
|
|
| Workspace switching | Solid → passes result to React via island props | React | React |
|
|
| Routing | Solid router | Hybrid: Solid routes to React islands | React router |
|
|
| Platform (Tauri IPC) | Solid platform provider | Framework-agnostic adapter module | React calls adapter directly |
|
|
| Settings/config | Solid | Migrated domain by domain | React |
|
|
|
|
---
|
|
|
|
## Bridge Contract (Shrinks Over Time)
|
|
|
|
The island props are the formal contract between Solid and React. It starts small and shrinks to zero:
|
|
|
|
```ts
|
|
// Stage 1 — React island gets minimal props from Solid
|
|
interface IslandProps {
|
|
workspaceUrl: string
|
|
workspaceToken: string
|
|
workspaceId: string
|
|
sessionId: string | null
|
|
onNavigate: (path: string) => void // React tells Solid to route
|
|
}
|
|
|
|
// Stage 2 — React takes over sidebar, fewer props needed
|
|
interface IslandProps {
|
|
workspaces: WorkspaceConnection[] // React now owns selection
|
|
onNavigate: (path: string) => void
|
|
}
|
|
|
|
// Stage 3 — no island, no props. React owns everything.
|
|
// island.tsx deleted, solid-js removed.
|
|
```
|
|
|
|
Each time a surface migrates, the island props shrink. When they hit zero, the island is removed.
|
|
|
|
---
|
|
|
|
## File Structure (CUPID Domains, Component-Enclosed State)
|
|
|
|
Mirrors the existing CUPID domain map. Each domain colocates state, data, and UI. Components own the state they render — "general" session state sits at the session boundary, local UI state lives inside the component that needs it.
|
|
|
|
```
|
|
apps/app/src/react/
|
|
├── README.md # Why this exists, how to enable, migration status
|
|
│
|
|
├── island.tsx # Solid→React bridge (mounts boot.tsx into a DOM node)
|
|
├── boot.tsx # React root, providers, top-level wiring
|
|
├── feature-flag.ts # Read/write opt-in flag
|
|
│
|
|
├── kernel/ # Smallest shared layer (CUPID kernel rules apply)
|
|
│ ├── opencode-client.ts # Plain fetch() for OpenCode proxy — no Solid dependency
|
|
│ ├── types.ts # Session, Message, Workspace shapes
|
|
│ ├── query-provider.tsx # react-query provider + defaults
|
|
│ └── dev-panel.tsx # Dev-only: renderSource, transition, timings
|
|
│
|
|
├── shell/ # App-wide composition only (thin)
|
|
│ ├── layout.tsx # Sidebar + main area composition
|
|
│ ├── router.tsx # Route → domain view dispatch
|
|
│ └── index.ts
|
|
│
|
|
├── session/ # Domain: active task/session experience
|
|
│ │
|
|
│ │ -- General session state (shared by session components) --
|
|
│ ├── session-store.ts # renderedSessionId, intendedSessionId, renderSource
|
|
│ ├── transition-controller.ts # idle → switching → cache → live → idle
|
|
│ ├── sessions-query.ts # react-query: list sessions for a workspace
|
|
│ ├── session-snapshot-query.ts # react-query: full session + messages
|
|
│ │
|
|
│ │ -- Session view (composition root for the main area) --
|
|
│ ├── session-view.tsx # Composes message-list + composer + status
|
|
│ │ # owns: scroll position, view-level layout
|
|
│ │
|
|
│ │ -- Message list (owns its own scroll/virtualization) --
|
|
│ ├── message-list/
|
|
│ │ ├── message-list.tsx # Virtualized container
|
|
│ │ │ # owns: virtualization state, scroll anchor
|
|
│ │ ├── message-item.tsx # Single message bubble
|
|
│ │ │ # owns: collapsed/expanded, copy state
|
|
│ │ ├── part-view.tsx # Tool call, text, file, reasoning
|
|
│ │ │ # owns: expand/collapse per part
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ │ -- Composer (owns its own input state) --
|
|
│ ├── composer/
|
|
│ │ ├── composer.tsx # Prompt textarea + attachments + run/abort
|
|
│ │ │ # owns: draft text, file list, submitting
|
|
│ │ ├── send-prompt.ts # Mutation: send, SSE subscribe, abort
|
|
│ │ ├── attachment-picker.tsx
|
|
│ │ │ # owns: file picker open/selected state
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ │ -- Session sidebar (owns its own list state) --
|
|
│ ├── session-sidebar/
|
|
│ │ ├── session-sidebar.tsx # Session list for one workspace
|
|
│ │ │ # owns: search filter, rename-in-progress
|
|
│ │ ├── session-item.tsx # Single row
|
|
│ │ │ # owns: hover, context menu open
|
|
│ │ └── index.ts
|
|
│ │
|
|
│ │ -- Transition UX --
|
|
│ ├── transition-overlay.tsx # "Switching..." / skeleton during transitions
|
|
│ │ # owns: nothing — reads from transition-controller
|
|
│ │
|
|
│ └── index.ts # Public surface (only what shell needs)
|
|
│
|
|
├── workspace/ # Domain: workspace lifecycle
|
|
│ ├── workspace-store.ts # Which workspaces exist, connection info
|
|
│ ├── workspace-list.tsx # Sidebar workspace groups
|
|
│ │ # owns: collapsed state, selection highlight
|
|
│ ├── workspace-switcher.tsx # Switching logic + transition state
|
|
│ │ # owns: switching/idle/failed for workspace changes
|
|
│ ├── workspaces-query.ts # react-query: list + status
|
|
│ ├── create-workspace-modal.tsx # Add workspace flow
|
|
│ └── index.ts
|
|
│
|
|
├── connections/ # Domain: providers, MCP
|
|
│ └── index.ts # Placeholder — empty until needed
|
|
│
|
|
├── cloud/ # Domain: hosted workers, den
|
|
│ └── index.ts # Placeholder — empty until needed
|
|
│
|
|
├── app-settings/ # Domain: preferences, themes
|
|
│ └── index.ts # Placeholder — empty until needed
|
|
│
|
|
└── automations/ # Domain: scheduled jobs
|
|
└── index.ts # Placeholder — empty until needed
|
|
```
|
|
|
|
### Component-enclosed state hierarchy
|
|
|
|
Visual hierarchy = state hierarchy. A human reading the tree knows who owns what:
|
|
|
|
```
|
|
shell/layout.tsx
|
|
├── workspace/workspace-list.tsx → owns: selection, collapse
|
|
│ └── workspace-switcher.tsx → owns: workspace transition state
|
|
│
|
|
└── session/session-view.tsx → reads: session-store (general)
|
|
├── session/message-list/ → owns: scroll, virtualization
|
|
│ └── message-item.tsx → owns: expand/collapse per message
|
|
│ └── part-view.tsx → owns: expand/collapse per part
|
|
├── session/composer/ → owns: draft, files, submitting
|
|
├── session/session-sidebar/ → owns: search, rename-in-progress
|
|
└── session/transition-overlay.tsx → reads: transition-controller (no local state)
|
|
```
|
|
|
|
General session state (`session-store.ts`, `transition-controller.ts`, queries) lives at the `session/` root — shared by components below it. Component-local state (draft text, scroll position, expand/collapse) lives inside the component that renders it. No ambiguity.
|
|
|
|
---
|
|
|
|
## Styling Strategy
|
|
|
|
### What carries over for free
|
|
|
|
The entire styling foundation is framework-agnostic. React components inherit everything without configuration:
|
|
|
|
| Asset | Framework-dependent? | Notes |
|
|
|-------|---------------------|-------|
|
|
| Tailwind classes | No | Just CSS strings. Same classes, same output. |
|
|
| CSS custom properties (700+ Radix + DLS tokens) | No | Pure CSS, loaded in `index.css`. |
|
|
| Dark mode (`data-theme` + variable swap) | No | Works on any DOM element. |
|
|
| `ow-*` CSS classes (buttons, cards, pills) | No | Global CSS, available everywhere. |
|
|
| `@keyframes` animations | No | Pure CSS. |
|
|
| Font stack | No | System fonts, nothing to load. |
|
|
| `DESIGN-LANGUAGE.md` reference | No | Design rules are visual, not framework. |
|
|
|
|
**React components use `className=` instead of `class=`. That is the only syntax change.**
|
|
|
|
### What to add for React
|
|
|
|
One utility in `react/kernel/`:
|
|
|
|
```ts
|
|
// react/kernel/cn.ts
|
|
import { clsx, type ClassValue } from "clsx";
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs));
|
|
}
|
|
```
|
|
|
|
The Solid side manages without this (template literals). The React side benefits from `cn()` for conditional classes — it's the standard React/Tailwind convention and prevents class conflicts during composition.
|
|
|
|
**New dependencies (Phase 0):** `clsx`, `tailwind-merge`.
|
|
|
|
### Styling rules for React components
|
|
|
|
1. **Use the same Tailwind classes.** Reference `DESIGN-LANGUAGE.md` for visual decisions.
|
|
2. **Use DLS tokens** (`dls-surface`, `dls-border`, `dls-accent`, etc.) via Tailwind config, not raw hex values.
|
|
3. **Use Radix color scales** (`bg-gray-3`, `text-blue-11`) for non-semantic colors.
|
|
4. **Use `ow-*` classes** where they exist (e.g., `ow-button-primary`, `ow-soft-card`).
|
|
5. **Use `cn()`** for conditional classes instead of template literals.
|
|
6. **No CSS-in-JS.** No styled-components, no emotion. Tailwind only.
|
|
7. **No `dark:` prefix.** Dark mode is handled by CSS variable swap on `[data-theme="dark"]`.
|
|
8. **Animation is CSS-only.** Use Tailwind `transition-*` and the existing custom `@keyframes`. No framer-motion.
|
|
9. **Match the Solid component's visual output exactly.** When migrating a surface, screenshot both versions and diff. Same spacing, same colors, same radius, same shadows.
|
|
|
|
### Visual parity verification
|
|
|
|
Each migrated surface gets a visual comparison test:
|
|
1. Screenshot the Solid version (Chrome DevTools).
|
|
2. Screenshot the React version (same viewport, same data).
|
|
3. Overlay or side-by-side compare. No visible difference = pass.
|
|
|
|
This is added to the test actions for each phase.
|
|
|
|
---
|
|
|
|
## Isolation & Testing Strategy
|
|
|
|
### Per-phase Docker isolation
|
|
Each phase gets tested against two independent Docker dev stacks:
|
|
|
|
```
|
|
Stack A (control): runs the existing SolidJS app
|
|
→ packaging/docker/dev-up.sh → server :PORT_A, web :PORT_A_WEB
|
|
|
|
Stack B (experiment): independent server
|
|
→ packaging/docker/dev-up.sh → server :PORT_B, web :PORT_B_WEB
|
|
```
|
|
|
|
Both stacks share the same repo (bind-mounted), but run independent servers with independent tokens and hostnames (verified via `hostname` command through the UI).
|
|
|
|
### Test actions
|
|
Every phase adds entries to `test-actions.md` with:
|
|
- Steps to exercise the new React surface
|
|
- Expected results
|
|
- Comparison against the Solid version on the control stack
|
|
- Chrome DevTools verification (using `functions.chrome-devtools_*`)
|
|
|
|
### Feature flag gate
|
|
```
|
|
localStorage.setItem('openwork:react-session', 'true')
|
|
// or
|
|
http://localhost:<WEB_PORT>/session?react=1
|
|
```
|
|
|
|
The app shell checks this flag and renders either:
|
|
- `<SessionView />` (Solid, existing)
|
|
- `<ReactIsland />` → React session view (new)
|
|
|
|
---
|
|
|
|
## Phase Roadmap
|
|
|
|
### Phase 0: Build Infrastructure
|
|
|
|
**Goal**: React components can render inside the SolidJS app.
|
|
|
|
**Deliverables**:
|
|
1. Add `@vitejs/plugin-react` to Vite config (alongside `vite-plugin-solid`).
|
|
2. File convention: `*.tsx` in `src/react/` = React. Everything else = Solid.
|
|
3. `island.tsx` — Solid component that mounts a React root into a DOM node.
|
|
4. `boot.tsx` — React root with `QueryClientProvider`.
|
|
5. Add `react`, `react-dom`, `@tanstack/react-query` to `apps/app/package.json`.
|
|
6. `feature-flag.ts` — reads localStorage / query param.
|
|
7. Verify: a trivial React component renders inside the Solid shell.
|
|
|
|
**Test**:
|
|
- Boot Docker stack.
|
|
- Navigate to session view.
|
|
- Enable feature flag.
|
|
- Confirm React island mounts (check React DevTools or a visible test banner).
|
|
|
|
**Does NOT change any user-visible behavior.**
|
|
|
|
### Phase 1: React Session View (Read-Only)
|
|
|
|
**Goal**: A React component can display a session's messages (read-only, no composer).
|
|
|
|
**Deliverables**:
|
|
1. `react/kernel/opencode-client.ts` — plain `fetch()` client for OpenCode proxy.
|
|
2. `react/kernel/types.ts` — Session, Message, Part shapes.
|
|
3. `react/session/session-store.ts` — `renderedSessionId`, `intendedSessionId`, `renderSource`.
|
|
4. `react/session/sessions-query.ts` — react-query: list sessions.
|
|
5. `react/session/session-snapshot-query.ts` — react-query: session + messages.
|
|
6. `react/session/session-view.tsx` — composition root.
|
|
7. `react/session/message-list/` — virtualized message rendering.
|
|
8. Feature-flagged: `?react=1` shows React view, default shows Solid.
|
|
|
|
**State ownership**: React owns all session read state. It fetches directly from the OpenCode proxy. No Solid signal subscriptions. The island props provide only: `workspaceUrl`, `workspaceToken`, `workspaceId`, `sessionId`.
|
|
|
|
**Test actions**:
|
|
- Create session in Solid view, send a prompt, get a response.
|
|
- Switch to React view (`?react=1`) — same session's messages appear.
|
|
- Switch sessions — React view transitions without white screen.
|
|
- Compare: Solid view on Stack A, React view on Stack B, same prompt, same output.
|
|
|
|
**Success criteria**:
|
|
- No blank pane during session switch.
|
|
- Messages render from cache instantly, upgrade to live data.
|
|
- `renderSource` visible in dev panel.
|
|
|
|
### Phase 2: React Composer (Send/Receive)
|
|
|
|
**Goal**: The React session view can send prompts and display streaming responses.
|
|
|
|
**Deliverables**:
|
|
1. `react/session/composer/composer.tsx` — prompt input, file attachment, run/abort.
|
|
2. `react/session/composer/send-prompt.ts` — mutation: send, SSE stream, abort.
|
|
3. `react/session/composer/attachment-picker.tsx`.
|
|
4. SSE subscription for streaming message parts.
|
|
5. `streamdown` for markdown rendering of streaming text.
|
|
|
|
**State ownership**: Composer owns draft text, file list, submitting state. Send mutation is local to composer. Streaming messages flow into react-query cache via SSE → cache invalidation.
|
|
|
|
**Test actions**:
|
|
- Type a prompt in React composer, click Run.
|
|
- Response streams in real-time.
|
|
- Abort mid-stream — session stops cleanly.
|
|
- Switch workspace mid-stream — no crash.
|
|
|
|
**Success criteria**:
|
|
- Full send/receive/abort cycle works in React view.
|
|
- Streaming feels identical to Solid view.
|
|
|
|
### Phase 3: Transition Controller + Debug Panel
|
|
|
|
**Goal**: The React path handles workspace and session switching with explicit transition states.
|
|
|
|
**Deliverables**:
|
|
1. `react/session/transition-controller.ts` — state machine:
|
|
```
|
|
idle → switching → (cache-render) → (live-upgrade) → idle
|
|
idle → switching → failed → recovering → idle
|
|
```
|
|
2. `react/session/transition-overlay.tsx` — skeleton/indicator during transitions.
|
|
3. `react/kernel/dev-panel.tsx` — shows `routeState`, `transitionState`, `renderSource`, `runtimeState`.
|
|
|
|
**Test actions**:
|
|
- Connect two Docker dev stacks as workspaces.
|
|
- Switch between workspaces rapidly.
|
|
- React view never shows white screen.
|
|
- Debug panel visible and accurate.
|
|
|
|
**Success criteria**:
|
|
- Zero white screens during any switch sequence.
|
|
- Transition states are inspectable via Chrome DevTools.
|
|
|
|
### Phase 4: Backend Read APIs (parallel track)
|
|
|
|
**Goal**: Session reads don't require client-side OpenCode proxy orchestration.
|
|
|
|
**Deliverables** (in `apps/server/src/server.ts`):
|
|
1. `GET /workspace/:id/sessions` — list sessions for a workspace.
|
|
2. `GET /workspace/:id/sessions/:sessionId` — session detail with messages.
|
|
3. `GET /workspace/:id/sessions/:sessionId/snapshot` — full session snapshot.
|
|
4. Typed response schemas (zod).
|
|
|
|
**Test actions**:
|
|
- `curl http://localhost:<PORT>/workspace/<id>/sessions` returns session list.
|
|
- `curl http://localhost:<PORT>/workspace/<id>/sessions/<sid>/snapshot` returns full snapshot.
|
|
- Works for any workspace, not just the "active" one.
|
|
- React query layer switches to these endpoints.
|
|
|
|
**Success criteria**:
|
|
- Session reads work without activation.
|
|
- Response times < 100ms for cached reads.
|
|
|
|
### Phase 5: React Session as Default
|
|
|
|
**Goal**: Flip the feature flag. React session view is the default.
|
|
|
|
**Deliverables**:
|
|
1. Feature flag default flips to `true`.
|
|
2. `?solid=1` to opt back into Solid session view.
|
|
3. Remove any Solid↔React shims that are no longer needed for session.
|
|
|
|
**Success criteria**:
|
|
- All test actions pass with React as default.
|
|
- No regression in any existing flow.
|
|
|
|
### Phase 6: Migrate Workspace Sidebar
|
|
|
|
**Goal**: React owns the workspace list and session sidebar.
|
|
|
|
**Deliverables**:
|
|
1. `react/workspace/workspace-list.tsx` — workspace groups in sidebar.
|
|
2. `react/session/session-sidebar/` — session list per workspace.
|
|
3. `react/workspace/workspace-switcher.tsx` — switching logic.
|
|
4. Island props shrink: React now receives `workspaces[]` instead of single workspace info.
|
|
|
|
**State ownership**: React owns workspace selection, sidebar collapse, session list filtering. Solid still owns settings and platform.
|
|
|
|
### Phase 7: Migrate Settings & Connections
|
|
|
|
**Goal**: React owns settings pages and provider/MCP flows.
|
|
|
|
**Deliverables**:
|
|
1. Fill `react/app-settings/` — theme, preferences, config.
|
|
2. Fill `react/connections/` — provider auth, MCP.
|
|
3. Fill `react/cloud/` — hosted workers, den.
|
|
|
|
### Phase 8: Island Inversion
|
|
|
|
**Goal**: React becomes the shell. Solid becomes the guest (if anything remains).
|
|
|
|
**Deliverables**:
|
|
1. `react/shell/layout.tsx` becomes the top-level composition.
|
|
2. `react/shell/router.tsx` owns all routing.
|
|
3. If any Solid surfaces remain, they render inside a `SolidIsland` React component.
|
|
4. Island props are now zero or near-zero.
|
|
|
|
### Phase 9: Remove Solid
|
|
|
|
**Goal**: The app is pure React.
|
|
|
|
**Deliverables**:
|
|
1. Remove `vite-plugin-solid` from Vite config.
|
|
2. Remove `solid-js`, `@solidjs/router`, `solid-primitives` from `package.json`.
|
|
3. Delete `apps/app/src/app/` (the old Solid tree).
|
|
4. `apps/app/src/react/` becomes `apps/app/src/app/` (or stays where it is).
|
|
5. Remove `island.tsx`, `feature-flag.ts`.
|
|
|
|
---
|
|
|
|
## Migration Surface Order
|
|
|
|
```
|
|
Phase 0-3 → Session view (messages, composer, transitions)
|
|
Phase 5 → Flip session default to React
|
|
Phase 6 → Workspace sidebar + session sidebar
|
|
← tipping point: React owns enough to invert the island →
|
|
Phase 7 → Settings, connections, cloud
|
|
Phase 8 → Shell/layout/routing — island inversion
|
|
Phase 9 → Remove Solid entirely
|
|
```
|
|
|
|
## Timeline Guidance
|
|
|
|
| Phase | Scope | Estimated Effort |
|
|
|-------|-------|-----------------|
|
|
| 0 | Build infra | ~1 day |
|
|
| 1 | Read-only session view | ~1 week |
|
|
| 2 | Composer + streaming | ~1 week |
|
|
| 3 | Transition controller + debug | ~1 week |
|
|
| 4 | Backend read APIs (parallel) | ~1 week |
|
|
| 5 | Flip session default | ~1 day |
|
|
| 6 | Workspace sidebar | ~1 week |
|
|
| 7 | Settings, connections, cloud | ~2-3 weeks |
|
|
| 8 | Island inversion | ~1 week |
|
|
| 9 | Remove Solid | ~1 day |
|
|
|
|
Phases 0-3 are fast and highly visible. Phase 4 can run in parallel. Phases 6+ can be paced based on stability.
|
|
|
|
---
|
|
|
|
## Files Changed Per Phase
|
|
|
|
| Phase | Files |
|
|
|-------|-------|
|
|
| 0 | `apps/app/vite.config.ts`, `apps/app/package.json`, new `src/react/island.tsx`, `src/react/boot.tsx`, `src/react/feature-flag.ts` |
|
|
| 1 | New `src/react/kernel/` (3 files), new `src/react/session/` (6-8 files), feature flag check in `app.tsx` |
|
|
| 2 | New `src/react/session/composer/` (3 files) |
|
|
| 3 | New `src/react/session/transition-controller.ts`, `transition-overlay.tsx`, `src/react/kernel/dev-panel.tsx` |
|
|
| 4 | `apps/server/src/server.ts` (add 3-4 endpoints), new `apps/server/src/session-read-model.ts` |
|
|
| 5 | `app.tsx` flag flip, cleanup |
|
|
| 6 | New `src/react/workspace/` (4-5 files), `src/react/session/session-sidebar/` (2 files) |
|
|
| 7 | Fill `src/react/connections/`, `src/react/app-settings/`, `src/react/cloud/` |
|
|
| 8 | `src/react/shell/` becomes the root, island inversion |
|
|
| 9 | Delete `src/app/`, remove Solid deps |
|
|
|
|
---
|
|
|
|
## Verification Approach
|
|
|
|
Every phase:
|
|
1. Boot two Docker dev stacks (`dev-up.sh` x2).
|
|
2. Connect Stack B as a workspace from Stack A's UI.
|
|
3. Run the phase's test actions via Chrome DevTools (`functions.chrome-devtools_*`).
|
|
4. Screenshot evidence saved to repo.
|
|
5. Update `test-actions.md` with the new test actions.
|
|
6. PR includes screenshots and test action references.
|
|
|
|
---
|
|
|
|
## Dependency Direction
|
|
|
|
Same CUPID rules apply to the React tree:
|
|
|
|
```
|
|
shell → domain public API (index.ts) → domain internals
|
|
```
|
|
|
|
- Domains may depend on `kernel/` primitives.
|
|
- Domains never reach into another domain's internals.
|
|
- Cross-domain imports go through `index.ts`.
|
|
- No bidirectional imports.
|
|
- No "super util" files.
|
|
|
|
---
|
|
|
|
## Anti-Patterns
|
|
|
|
- Adding feature logic to `shell/layout.tsx` (shell orchestrates, doesn't absorb).
|
|
- Sharing state between Solid and React for the same concern (one owner always).
|
|
- Creating `utils/` or `helpers/` buckets instead of colocating with the owning domain.
|
|
- Migrating more than one domain per phase.
|
|
- Rewriting Solid component behavior during migration (preserve behavior, change placement).
|
|
|
|
---
|
|
|
|
## Decision Heuristic
|
|
|
|
- **Immediate product feel**: start with frontend Phase 0-1 (session view).
|
|
- **Highest compounding win**: invest in backend Phase 4 (read APIs) in parallel.
|
|
- **When to invert the island**: after workspace sidebar (Phase 6) moves to React — that's when React owns enough of the visual hierarchy to be the shell.
|
|
- **When to remove Solid**: only after all domains are migrated and stable. Not before.
|