| ... | ... | @@ -1,564 +0,0 @@ |
| 1 | | # Plan Mode Deep Dive |
| 2 | | |
| 3 | | ## Goal |
| 4 | | |
| 5 | | Implement plan mode without fighting the architecture that already exists in this repo. |
| 6 | | |
| 7 | | The current app is: |
| 8 | | |
| 9 | | - provider-adapter driven on the server |
| 10 | | - orchestration snapshot driven on the client |
| 11 | | - already equipped with some plan-adjacent runtime events |
| 12 | | - not yet equipped with first-class plan-mode state in the orchestration read model |
| 13 | | |
| 14 | | This document updates the implementation plan to match that reality. |
| 15 | | |
| 16 | | --- |
| 17 | | |
| 18 | | ## Current Repo Baseline |
| 19 | | |
| 20 | | ### 1. Shared contracts already have some plan primitives |
| 21 | | |
| 22 | | `packages/contracts/src/providerRuntime.ts` already includes: |
| 23 | | |
| 24 | | - `turn.plan.updated` |
| 25 | | - `content.delta` with `streamKind: "plan_text"` |
| 26 | | - `user-input.requested` |
| 27 | | - `user-input.resolved` |
| 28 | | |
| 29 | | Important constraint: |
| 30 | | |
| 31 | | - these are runtime-event primitives only |
| 32 | | - there is no first-class provider capability schema |
| 33 | | - there is no first-class plan operating mode field |
| 34 | | - there is no dedicated final-plan artifact schema |
| 35 | | |
| 36 | | ### 2. Existing `runtimeMode` is not plan mode |
| 37 | | |
| 38 | | `RuntimeMode` in `packages/contracts/src/orchestration.ts` and `packages/contracts/src/provider.ts` means execution policy: |
| 39 | | |
| 40 | | - `approval-required` |
| 41 | | - `full-access` |
| 42 | | |
| 43 | | Plan mode must not overload that field. |
| 44 | | |
| 45 | | If we need provider operating mode, it needs a separate concept and separate naming, for example: |
| 46 | | |
| 47 | | - `interactionMode` |
| 48 | | - `planningMode` |
| 49 | | - `providerOperatingMode` |
| 50 | | |
| 51 | | Anything is acceptable as long as it is not reused from the existing access-policy `runtimeMode`. |
| 52 | | |
| 53 | | ### 3. Server flow is already canonical-event-first |
| 54 | | |
| 55 | | The current server pipeline is: |
| 56 | | |
| 57 | | 1. Provider adapters emit `ProviderRuntimeEvent`s. |
| 58 | | 2. `ProviderService` multiplexes adapter streams and maintains provider session bindings. |
| 59 | | 3. `ProviderRuntimeIngestion` translates runtime events into orchestration commands and thread activities. |
| 60 | | 4. `ProjectionPipeline` persists read-model tables. |
| 61 | | 5. `ProjectionSnapshotQuery` rebuilds the orchestration snapshot returned by `orchestration.getSnapshot`. |
| 62 | | 6. `wsServer` exposes generic orchestration snapshot, command dispatch, diff, and replay methods. |
| 63 | | |
| 64 | | This matters because plan mode should fit into the same pipeline, not bypass it with a parallel websocket protocol. |
| 65 | | |
| 66 | | Important detail: |
| 67 | | |
| 68 | | - `wsServer` broadcasts orchestration domain events, not raw provider runtime events |
| 69 | | - if the web should see plan state live, that state must first become orchestration events and/or projections |
| 70 | | |
| 71 | | ### 4. The current read model has nowhere to put plan mode |
| 72 | | |
| 73 | | `OrchestrationThread` currently contains: |
| 74 | | |
| 75 | | - `messages` |
| 76 | | - `activities` |
| 77 | | - `checkpoints` |
| 78 | | - `session` |
| 79 | | - `latestTurn` |
| 80 | | - thread metadata like title, model, branch, worktree |
| 81 | | |
| 82 | | It does not contain: |
| 83 | | |
| 84 | | - current structured plan snapshot |
| 85 | | - accumulated plan-text stream |
| 86 | | - pending structured user-input prompt |
| 87 | | - resolved structured answers |
| 88 | | - provider interactive capabilities |
| 89 | | - provider plan/default operating mode |
| 90 | | - final approved plan artifact |
| 91 | | |
| 92 | | Today the only flexible place plan data can survive into the web snapshot is `activities[].payload`. |
| 93 | | |
| 94 | | ### 5. The web app is snapshot-driven, not event-reduced |
| 95 | | |
| 96 | | The client flow today is: |
| 97 | | |
| 98 | | 1. `EventRouter` listens to `orchestration.domainEvent`. |
| 99 | | 2. On domain events, it re-fetches `orchestration.getSnapshot`. |
| 100 | | 3. Zustand stores the latest normalized snapshot. |
| 101 | | 4. `session-logic.ts` derives UI state like pending approvals and work log from thread activities. |
| 102 | | 5. `ChatView` renders the thread from snapshot state plus those derived selectors. |
| 103 | | |
| 104 | | Important implication: |
| 105 | | |
| 106 | | - plan mode should be added to the orchestration snapshot and/or activity derivation path |
| 107 | | - it should not depend on a separate client-side event reducer |
| 108 | | |
| 109 | | ### 6. Current implementation status by provider |
| 110 | | |
| 111 | | #### Codex |
| 112 | | |
| 113 | | Currently implemented: |
| 114 | | |
| 115 | | - `turn/plan/updated` maps to `turn.plan.updated` |
| 116 | | - `item/plan/delta` maps to `content.delta` with `streamKind: "plan_text"` |
| 117 | | |
| 118 | | Current gap: |
| 119 | | |
| 120 | | - `item/tool/requestUserInput` is currently classified as `request.opened` with request type `tool_user_input` |
| 121 | | - it is not yet upgraded into `user-input.requested` |
| 122 | | - there is no structured answer submission path yet |
| 123 | | |
| 124 | | #### Claude Code |
| 125 | | |
| 126 | | Currently implemented: |
| 127 | | |
| 128 | | - session/config metadata surfaces through `session.configured` |
| 129 | | - assistant/reasoning text flows through normal runtime content events |
| 130 | | - approval requests already bridge into `request.opened` / `request.resolved` |
| 131 | | |
| 132 | | Current gap: |
| 133 | | |
| 134 | | - no current canonical plan-mode mapping |
| 135 | | - no current `AskUserQuestion` mapping |
| 136 | | - no current `ExitPlanMode` mapping |
| 137 | | |
| 138 | | #### Cursor |
| 139 | | |
| 140 | | Currently implemented: |
| 141 | | |
| 142 | | - `available_commands_update` maps to `session.configured` |
| 143 | | - thought/message chunks map to `content.delta` |
| 144 | | - permission requests bridge into approval request events |
| 145 | | |
| 146 | | Current gap: |
| 147 | | |
| 148 | | - no current plan-mode mapping |
| 149 | | - no current structured prompt mapping |
| 150 | | - no current todo/plan projection |
| 151 | | |
| 152 | | --- |
| 153 | | |
| 154 | | ## Design Constraints |
| 155 | | |
| 156 | | ### Keep the current transport model |
| 157 | | |
| 158 | | Plan mode should continue to flow through: |
| 159 | | |
| 160 | | - `ProviderRuntimeEvent` |
| 161 | | - orchestration commands/events |
| 162 | | - projection tables |
| 163 | | - `orchestration.getSnapshot` |
| 164 | | - `orchestration.domainEvent` |
| 165 | | |
| 166 | | Do not introduce provider-specific websocket channels unless the current architecture proves insufficient. |
| 167 | | |
| 168 | | ### Keep the client snapshot-driven |
| 169 | | |
| 170 | | The client already assumes that server state is re-synced from snapshots. Plan mode should respect that. |
| 171 | | |
| 172 | | That means: |
| 173 | | |
| 174 | | - server-side persistence matters more than clever client buffering |
| 175 | | - reconnect behavior should come from projection state, not ad hoc React state |
| 176 | | - `session-logic.ts` and `ChatView` are the main frontend extension seams |
| 177 | | |
| 178 | | ### Keep approvals separate from structured user input |
| 179 | | |
| 180 | | The current system has a full approval pipeline: |
| 181 | | |
| 182 | | - runtime `request.opened` / `request.resolved` |
| 183 | | - orchestration `thread.approval.respond` |
| 184 | | - provider `respondToRequest(...)` |
| 185 | | |
| 186 | | Plan-mode structured prompts are different and should not be forced into the approval model unless a provider genuinely models them as approval requests. |
| 187 | | |
| 188 | | ### Preserve raw provider payloads |
| 189 | | |
| 190 | | When adding plan-mode mappings: |
| 191 | | |
| 192 | | - keep `raw` on internal canonical runtime events |
| 193 | | - keep `providerRefs` on internal canonical runtime events |
| 194 | | - avoid throwing away native payload shape too early inside adapter/server processing |
| 195 | | |
| 196 | | This does not mean provider-specific payloads should leak into the orchestration read model, websocket API, or web UI. |
| 197 | | |
| 198 | | The boundary should stay: |
| 199 | | |
| 200 | | - adapter/server internals may retain native payloads for correlation, debugging, and future parser improvements |
| 201 | | - shared orchestration state and client-visible contracts should expose only canonical plan-mode data |
| 202 | | |
| 203 | | That matters most for Claude and Cursor, where some mappings will be adapter-derived and may need native context during implementation and debugging. |
| 204 | | |
| 205 | | --- |
| 206 | | |
| 207 | | ## Updated Shared Work Plan |
| 208 | | |
| 209 | | ## 1. Tighten shared contract naming before wiring features |
| 210 | | |
| 211 | | The first step is not to invent a new architecture. It is to formalize the concepts that are currently missing from contracts. |
| 212 | | |
| 213 | | ### Add a plan operating mode concept |
| 214 | | |
| 215 | | Add a new shared contract for provider plan/default mode. |
| 216 | | |
| 217 | | Requirements: |
| 218 | | |
| 219 | | - do not reuse `RuntimeMode` |
| 220 | | - keep it orthogonal to access policy |
| 221 | | - make it available in the server->web read model, not only in raw runtime payloads |
| 222 | | |
| 223 | | Preferred shape: |
| 224 | | |
| 225 | | ```ts |
| 226 | | type ProviderInteractionMode = "default" | "plan"; |
| 227 | | ``` |
| 228 | | |
| 229 | | ### Add typed provider interactive capabilities |
| 230 | | |
| 231 | | Current `session.configured.payload.config` is just `Record<string, unknown>`. That is too weak for capability-driven UI. |
| 232 | | |
| 233 | | Add a typed capability surface in `packages/contracts` that both server and web can import directly. |
| 234 | | |
| 235 | | Preferred shape: |
| 236 | | |
| 237 | | - exported constant/object keyed by `ProviderKind` |
| 238 | | - exported type derived from that object |
| 239 | | - usable by adapters, orchestration code, and frontend rendering logic without duplicating provider capability tables |
| 240 | | |
| 241 | | Minimum fields: |
| 242 | | |
| 243 | | ```ts |
| 244 | | interface ProviderInteractiveCapabilities { |
| 245 | | supportsPlanMode: boolean; |
| 246 | | supportsStructuredPlanUpdates: boolean; |
| 247 | | supportsPlanTextStreaming: boolean; |
| 248 | | supportsStructuredUserInput: boolean; |
| 249 | | supportsFreeformUserInput: boolean; |
| 250 | | supportsPlanAcceptance: boolean; |
| 251 | | } |
| 252 | | ``` |
| 253 | | |
| 254 | | Preferred usage: |
| 255 | | |
| 256 | | - static provider capabilities live in shared contracts |
| 257 | | - adapters may still emit dynamic runtime metadata when a capability is session-specific or probe-dependent |
| 258 | | - the orchestration snapshot should expose the active provider and any dynamic overrides, not duplicate the full static capability catalog per thread |
| 259 | | |
| 260 | | ### Add a dedicated structured-answer command path |
| 261 | | |
| 262 | | Current shared commands only support approvals: |
| 263 | | |
| 264 | | - `thread.approval.respond` |
| 265 | | - provider `respondToRequest(...)` |
| 266 | | |
| 267 | | Plan mode needs a distinct path for structured prompt answers. |
| 268 | | |
| 269 | | Preferred additions: |
| 270 | | |
| 271 | | - orchestration command such as `thread.user-input.respond` |
| 272 | | - provider service method such as `respondToUserInput(...)` |
| 273 | | - adapter hook per provider for native answer serialization |
| 274 | | |
| 275 | | Do not overload approval decisions like `accept` / `decline` for structured question answers. |
| 276 | | |
| 277 | | ## 2. Extend the orchestration read model |
| 278 | | |
| 279 | | The current read model is the main missing piece. |
| 280 | | |
| 281 | | Plan mode needs first-class projection state so reconnect and refresh behave like the rest of the app. |
| 282 | | |
| 283 | | ### Add first-class thread plan state |
| 284 | | |
| 285 | | Preferred thread-level additions: |
| 286 | | |
| 287 | | - `interactionMode` |
| 288 | | - `currentPlan` |
| 289 | | - `pendingUserInput` |
| 290 | | - `resolvedUserInputs` |
| 291 | | - optional `finalPlanArtifact` |
| 292 | | |
| 293 | | The exact nesting can vary, but it should be snapshot-friendly and not require replaying arbitrary activity payloads in the client. |
| 294 | | |
| 295 | | `interactiveCapabilities` should not be modeled as thread-owned state if they are static per provider. |
| 296 | | |
| 297 | | Preferred split: |
| 298 | | |
| 299 | | - static capabilities: shared contract export keyed by provider |
| 300 | | - dynamic per-thread/per-session state: only fields that can actually vary at runtime, such as current interaction mode or active pending prompt |
| 301 | | |
| 302 | | ### Keep activities as secondary audit trail |
| 303 | | |
| 304 | | `thread.activities` should still receive plan/user-input activity summaries for timeline/debugging, but it should not be the only source of truth for active plan state. |
| 305 | | |
| 306 | | ### Projection changes |
| 307 | | |
| 308 | | This likely means: |
| 309 | | |
| 310 | | - new projection repository/table(s) for plan state and prompt state |
| 311 | | - `ProjectionPipeline` updates |
| 312 | | - `ProjectionSnapshotQuery` returning the new fields |
| 313 | | - projection tests for persistence and replay |
| 314 | | |
| 315 | | ## 3. Update runtime ingestion to populate that state |
| 316 | | |
| 317 | | `ProviderRuntimeIngestion` already knows how to: |
| 318 | | |
| 319 | | - turn assistant text into messages |
| 320 | | - turn approval events into activities |
| 321 | | - turn `turn.plan.updated` into an activity |
| 322 | | |
| 323 | | It should be extended to also translate plan/user-input runtime events into read-model state. |
| 324 | | |
| 325 | | Implementation direction: |
| 326 | | |
| 327 | | - `turn.plan.updated` updates projected plan snapshot |
| 328 | | - `content.delta(plan_text)` appends to projected plan-text buffer when relevant |
| 329 | | - `user-input.requested` opens a projected pending prompt |
| 330 | | - `user-input.resolved` closes/resolves that prompt |
| 331 | | - providers that only expose raw/native blobs can still emit synthesized canonical runtime events first, then rely on the shared ingestion path |
| 332 | | |
| 333 | | ## 4. Integrate plan mode into the existing web seams |
| 334 | | |
| 335 | | The web architecture already has the right places. |
| 336 | | |
| 337 | | ### Store and types |
| 338 | | |
| 339 | | Update: |
| 340 | | |
| 341 | | - `apps/web/src/types.ts` |
| 342 | | - `apps/web/src/store.ts` |
| 343 | | |
| 344 | | The store should stay thin and snapshot-oriented. It should sync whatever new read-model fields the server exposes. |
| 345 | | |
| 346 | | ### Derived UI state |
| 347 | | |
| 348 | | Update: |
| 349 | | |
| 350 | | - `apps/web/src/session-logic.ts` |
| 351 | | |
| 352 | | This is the natural place for: |
| 353 | | |
| 354 | | - `derivePlanState(...)` |
| 355 | | - `derivePendingUserInput(...)` |
| 356 | | - any fallback derivation from activities during an incremental migration |
| 357 | | |
| 358 | | ### Rendering |
| 359 | | |
| 360 | | Primary UI seam: |
| 361 | | |
| 362 | | - `apps/web/src/components/ChatView.tsx` |
| 363 | | |
| 364 | | Current UI already has: |
| 365 | | |
| 366 | | - top-of-thread alert stacks for pending approvals |
| 367 | | - work-log rendering derived from activities |
| 368 | | - timeline rows for messages and work state |
| 369 | | |
| 370 | | That suggests two viable rendering patterns: |
| 371 | | |
| 372 | | 1. active plan/prompt panel above the timeline |
| 373 | | 2. resolved plan/prompt entries rendered in timeline/history |
| 374 | | |
| 375 | | The route structure does not need to change. Plan mode should remain thread-local state inside the existing chat route. |
| 376 | | |
| 377 | | --- |
| 378 | | |
| 379 | | ## Provider Implementation Plan |
| 380 | | |
| 381 | | ## Provider 1: Codex |
| 382 | | |
| 383 | | Codex is closest to the target shape and should be the first end-to-end implementation. |
| 384 | | |
| 385 | | ### Current status |
| 386 | | |
| 387 | | Already present: |
| 388 | | |
| 389 | | - native structured plan update mapping |
| 390 | | - native plan-text stream mapping |
| 391 | | |
| 392 | | Missing: |
| 393 | | |
| 394 | | - native structured prompt mapping for `item/tool/requestUserInput` |
| 395 | | - structured answer submission path |
| 396 | | - removal of the current empty-answer auto-ack path in `codexAppServerManager` |
| 397 | | - projection and UI support for the already-emitted plan events |
| 398 | | |
| 399 | | ### Required work |
| 400 | | |
| 401 | | 1. Upgrade `item/tool/requestUserInput` from request-only handling to canonical `user-input.requested`. |
| 402 | | 2. Preserve `request.opened` as transport/debug metadata when useful. |
| 403 | | 3. Add adapter-side answer submission for the corresponding pending request/tool context. |
| 404 | | 4. Emit `user-input.resolved` when the answer is submitted or confirmed resolved. |
| 405 | | 5. Project the result into the orchestration snapshot. |
| 406 | | |
| 407 | | ### Codex-specific note |
| 408 | | |
| 409 | | The runtime event schemas already match Codex reasonably well, so this provider should define the canonical end-to-end behavior first. |
| 410 | | |
| 411 | | ## Provider 2: Claude Code |
| 412 | | |
| 413 | | Claude likely needs tool-aware adaptation rather than pure transport mapping. |
| 414 | | |
| 415 | | ### Current status |
| 416 | | |
| 417 | | Already present: |
| 418 | | |
| 419 | | - generic session/runtime stream integration |
| 420 | | - approval bridging |
| 421 | | |
| 422 | | Missing: |
| 423 | | |
| 424 | | - plan/default operating mode mapping |
| 425 | | - `AskUserQuestion` -> `user-input.requested` |
| 426 | | - `ExitPlanMode` -> final-plan or plan-state handoff |
| 427 | | |
| 428 | | ### Required work |
| 429 | | |
| 430 | | 1. Detect plan/default operating mode from Claude-native metadata without touching access-policy `runtimeMode`. |
| 431 | | 2. Map `AskUserQuestion` tool payloads into canonical `user-input.requested`. |
| 432 | | 3. Add provider-specific answer serialization for the return path. |
| 433 | | 4. Detect `ExitPlanMode` and decide whether it becomes: |
| 434 | | - a final-plan artifact |
| 435 | | - a synthetic structured plan snapshot |
| 436 | | - or both |
| 437 | | 5. Avoid over-synthesizing incremental structured plan steps unless Claude gives us enough structure to do it safely. |
| 438 | | |
| 439 | | ## Provider 3: Cursor |
| 440 | | |
| 441 | | Cursor should start from the same canonical server pipeline, but with a more conservative adapter. |
| 442 | | |
| 443 | | ### Current status |
| 444 | | |
| 445 | | Already present: |
| 446 | | |
| 447 | | - session/update stream handling |
| 448 | | - reasoning/assistant text mapping |
| 449 | | - approval bridging |
| 450 | | |
| 451 | | Missing: |
| 452 | | |
| 453 | | - plan/default operating mode mapping |
| 454 | | - structured prompt mapping |
| 455 | | - structured plan/todo mapping |
| 456 | | |
| 457 | | ### Required work |
| 458 | | |
| 459 | | 1. Detect and publish Cursor operating mode separately from access-policy runtime mode. |
| 460 | | 2. Map documented extension methods such as `cursor/ask_question` and `cursor/update_todos` only when payloads are confirmed. |
| 461 | | 3. Fall back to plan-mode indicator plus normal assistant/reasoning text if structured Cursor plan events are absent. |
| 462 | | 4. Preserve raw ACP payloads so the adapter can improve without changing higher layers. |
| 463 | | |
| 464 | | --- |
| 465 | | |
| 466 | | ## Frontend Rendering Plan |
| 467 | | |
| 468 | | The frontend should remain capability-driven, but using real repo seams. |
| 469 | | |
| 470 | | ### Active state |
| 471 | | |
| 472 | | Render from projected snapshot fields: |
| 473 | | |
| 474 | | - current plan/default operating mode |
| 475 | | - provider capabilities |
| 476 | | - current structured plan |
| 477 | | - current plan-text stream |
| 478 | | - pending structured prompt |
| 479 | | |
| 480 | | ### History state |
| 481 | | |
| 482 | | Render from: |
| 483 | | |
| 484 | | - normal messages |
| 485 | | - resolved prompt answers |
| 486 | | - plan-related activity/history rows |
| 487 | | - optional final-plan artifact |
| 488 | | |
| 489 | | ### Placement |
| 490 | | |
| 491 | | Recommended initial placement: |
| 492 | | |
| 493 | | 1. Active pending prompt and active plan panel above the timeline, near the current approvals stack. |
| 494 | | 2. Resolved prompt/plan history in the timeline. |
| 495 | | |
| 496 | | This fits the existing `ChatView` layout with the least architectural churn. |
| 497 | | |
| 498 | | --- |
| 499 | | |
| 500 | | ## Test Plan |
| 501 | | |
| 502 | | ### Contracts |
| 503 | | |
| 504 | | Add coverage for: |
| 505 | | |
| 506 | | - new interaction-mode schema |
| 507 | | - new capability schema |
| 508 | | - new structured-answer command schema |
| 509 | | - any read-model additions for active plan/prompt state |
| 510 | | |
| 511 | | Retain existing coverage for: |
| 512 | | |
| 513 | | - `turn.plan.updated` |
| 514 | | - `content.delta(plan_text)` |
| 515 | | - `user-input.requested` |
| 516 | | - `user-input.resolved` |
| 517 | | |
| 518 | | ### Server |
| 519 | | |
| 520 | | Add or extend tests for: |
| 521 | | |
| 522 | | - provider adapter plan/prompt mapping |
| 523 | | - `ProviderRuntimeIngestion` plan/prompt projection behavior |
| 524 | | - `ProjectionPipeline` persistence and replay |
| 525 | | - `ProjectionSnapshotQuery` returning active plan state |
| 526 | | - provider answer submission path |
| 527 | | |
| 528 | | ### Web |
| 529 | | |
| 530 | | Add or extend tests for: |
| 531 | | |
| 532 | | - `syncServerReadModel` with new plan fields |
| 533 | | - `session-logic.ts` derivation of active plan/prompt state |
| 534 | | - `ChatView` rendering of: |
| 535 | | - active plan panel |
| 536 | | - pending structured prompt |
| 537 | | - resolved prompt history |
| 538 | | - plan-text fallback |
| 539 | | |
| 540 | | --- |
| 541 | | |
| 542 | | ## Recommended Execution Order |
| 543 | | |
| 544 | | 1. Add shared contract types for interaction mode, capabilities, and structured-answer submission. |
| 545 | | 2. Extend orchestration read-model contracts and projection storage for active plan/prompt state. |
| 546 | | 3. Wire `ProviderRuntimeIngestion` into those projections. |
| 547 | | 4. Finish Codex end-to-end first, since it already emits most of the canonical runtime pieces. |
| 548 | | 5. Add frontend snapshot sync, derivation, and rendering in `types.ts`, `store.ts`, `session-logic.ts`, and `ChatView.tsx`. |
| 549 | | 6. Add Claude adapter mapping. |
| 550 | | 7. Add Cursor adapter mapping and fallback behavior. |
| 551 | | |
| 552 | | ## Short version |
| 553 | | |
| 554 | | The repo is already close in one specific sense: the runtime event vocabulary for plans and structured prompts exists. |
| 555 | | |
| 556 | | The real missing architecture is: |
| 557 | | |
| 558 | | - a separate plan/default operating mode concept |
| 559 | | - typed provider capabilities |
| 560 | | - a first-class structured-answer command path |
| 561 | | - read-model projection for active plan/prompt state |
| 562 | | - web rendering sourced from that snapshot state |
| 563 | | |
| 564 | | That is the shape the implementation should follow. |