authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 21:07:08-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
loge9655d43d506943925dcb1f1561d0f52843f30f6
treef5ae54c23903e5aba32d0173fb50520e4bee9c49
parentf0a7ddaf80561ab9f1bbb45eb569c73b70a40284

Show unavailable providers as coming-soon placeholders

- Keep Claude Code and Cursor in provider list as disabled options - Remove unused Claude agent SDK dependency from server - Delete stale plan-mode deep-dive doc

5 files changed, 39 insertions(+), 570 deletions(-)

apps/server/package.json-1
...@@ -22,7 +22,6 @@...@@ -22,7 +22,6 @@
22 "test": "vitest run"22 "test": "vitest run"
23 },23 },
24 "dependencies": {24 "dependencies": {
25 "@anthropic-ai/claude-agent-sdk": "^0.2.62",
26 "@effect/platform-node": "catalog:",25 "@effect/platform-node": "catalog:",
27 "@effect/sql-sqlite-bun": "catalog:",26 "@effect/sql-sqlite-bun": "catalog:",
28 "@pierre/diffs": "^1.1.0-beta.16",27 "@pierre/diffs": "^1.1.0-beta.16",
apps/web/src/components/ChatView.tsx+21-1
...@@ -5279,6 +5279,7 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -5279,6 +5279,7 @@ const MessagesTimeline = memo(function MessagesTimeline({
5279});5279});
52805280
5281const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => option.available);5281const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => option.available);
5282const UNAVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => !option.available);
5282const COMING_SOON_PROVIDER_OPTIONS = [5283const COMING_SOON_PROVIDER_OPTIONS = [
5283 { id: "opencode", label: "OpenCode", icon: OpenCodeIcon },5284 { id: "opencode", label: "OpenCode", icon: OpenCodeIcon },
5284 { id: "gemini", label: "Gemini", icon: Gemini },5285 { id: "gemini", label: "Gemini", icon: Gemini },
...@@ -5440,7 +5441,26 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -5440,7 +5441,26 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {
5440 </MenuSub>5441 </MenuSub>
5441 );5442 );
5442 })}5443 })}
5443 <MenuDivider />5444 {UNAVAILABLE_PROVIDER_OPTIONS.length > 0 && <MenuDivider />}
5445 {UNAVAILABLE_PROVIDER_OPTIONS.map((option) => {
5446 const OptionIcon = PROVIDER_ICON_BY_PROVIDER[option.value];
5447 return (
5448 <MenuItem key={option.value} disabled>
5449 <OptionIcon
5450 aria-hidden="true"
5451 className={cn(
5452 "size-4 shrink-0 opacity-80",
5453 option.value === "claudeCode" ? "" : "text-muted-foreground/85",
5454 )}
5455 />
5456 <span>{option.label}</span>
5457 <span className="ms-auto text-[11px] text-muted-foreground/80 uppercase tracking-[0.08em]">
5458 Coming soon
5459 </span>
5460 </MenuItem>
5461 );
5462 })}
5463 {UNAVAILABLE_PROVIDER_OPTIONS.length === 0 && <MenuDivider />}
5444 {COMING_SOON_PROVIDER_OPTIONS.map((option) => {5464 {COMING_SOON_PROVIDER_OPTIONS.map((option) => {
5445 const OptionIcon = option.icon;5465 const OptionIcon = option.icon;
5446 return (5466 return (
apps/web/src/session-logic.test.ts+16-4
...@@ -514,11 +514,23 @@ describe("isLatestTurnSettled", () => {...@@ -514,11 +514,23 @@ describe("isLatestTurnSettled", () => {
514});514});
515515
516describe("PROVIDER_OPTIONS", () => {516describe("PROVIDER_OPTIONS", () => {
517 it("only advertises codex in the stack base", () => {517 it("keeps Claude Code and Cursor visible as unavailable placeholders in the stack base", () => {
518 const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode");518 const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode");
519 const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor");519 const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor");
520 expect(PROVIDER_OPTIONS).toEqual([{ value: "codex", label: "Codex", available: true }]);520 expect(PROVIDER_OPTIONS).toEqual([
521 expect(claude).toBeUndefined();521 { value: "codex", label: "Codex", available: true },
522 expect(cursor).toBeUndefined();522 { value: "claudeCode", label: "Claude Code", available: false },
523 { value: "cursor", label: "Cursor", available: false },
524 ]);
525 expect(claude).toEqual({
526 value: "claudeCode",
527 label: "Claude Code",
528 available: false,
529 });
530 expect(cursor).toEqual({
531 value: "cursor",
532 label: "Cursor",
533 available: false,
534 });
523 });535 });
524});536});
apps/web/src/session-logic.ts+2
...@@ -16,6 +16,8 @@ export const PROVIDER_OPTIONS: Array<{...@@ -16,6 +16,8 @@ export const PROVIDER_OPTIONS: Array<{
16 available: boolean;16 available: boolean;
17}> = [17}> = [
18 { value: "codex", label: "Codex", available: true },18 { value: "codex", label: "Codex", available: true },
19 { value: "claudeCode", label: "Claude Code", available: false },
20 { value: "cursor", label: "Cursor", available: false },
19];21];
2022
21export interface WorkLogEntry {23export interface WorkLogEntry {
docs/plan-mode-deep-dive.md deleted-564
...@@ -1,564 +0,0 @@
1# Plan Mode Deep Dive
2
3## Goal
4
5Implement plan mode without fighting the architecture that already exists in this repo.
6
7The 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
14This 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
29Important 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
43Plan mode must not overload that field.
44
45If we need provider operating mode, it needs a separate concept and separate naming, for example:
46
47- `interactionMode`
48- `planningMode`
49- `providerOperatingMode`
50
51Anything 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
55The current server pipeline is:
56
571. Provider adapters emit `ProviderRuntimeEvent`s.
582. `ProviderService` multiplexes adapter streams and maintains provider session bindings.
593. `ProviderRuntimeIngestion` translates runtime events into orchestration commands and thread activities.
604. `ProjectionPipeline` persists read-model tables.
615. `ProjectionSnapshotQuery` rebuilds the orchestration snapshot returned by `orchestration.getSnapshot`.
626. `wsServer` exposes generic orchestration snapshot, command dispatch, diff, and replay methods.
63
64This matters because plan mode should fit into the same pipeline, not bypass it with a parallel websocket protocol.
65
66Important 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
82It 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
92Today 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
96The client flow today is:
97
981. `EventRouter` listens to `orchestration.domainEvent`.
992. On domain events, it re-fetches `orchestration.getSnapshot`.
1003. Zustand stores the latest normalized snapshot.
1014. `session-logic.ts` derives UI state like pending approvals and work log from thread activities.
1025. `ChatView` renders the thread from snapshot state plus those derived selectors.
103
104Important 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
113Currently implemented:
114
115- `turn/plan/updated` maps to `turn.plan.updated`
116- `item/plan/delta` maps to `content.delta` with `streamKind: "plan_text"`
117
118Current 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
126Currently 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
132Current gap:
133
134- no current canonical plan-mode mapping
135- no current `AskUserQuestion` mapping
136- no current `ExitPlanMode` mapping
137
138#### Cursor
139
140Currently 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
146Current 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
158Plan mode should continue to flow through:
159
160- `ProviderRuntimeEvent`
161- orchestration commands/events
162- projection tables
163- `orchestration.getSnapshot`
164- `orchestration.domainEvent`
165
166Do not introduce provider-specific websocket channels unless the current architecture proves insufficient.
167
168### Keep the client snapshot-driven
169
170The client already assumes that server state is re-synced from snapshots. Plan mode should respect that.
171
172That 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
180The current system has a full approval pipeline:
181
182- runtime `request.opened` / `request.resolved`
183- orchestration `thread.approval.respond`
184- provider `respondToRequest(...)`
185
186Plan-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
190When 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
196This does not mean provider-specific payloads should leak into the orchestration read model, websocket API, or web UI.
197
198The 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
203That 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
211The 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
215Add a new shared contract for provider plan/default mode.
216
217Requirements:
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
223Preferred shape:
224
225```ts
226type ProviderInteractionMode = "default" | "plan";
227```
228
229### Add typed provider interactive capabilities
230
231Current `session.configured.payload.config` is just `Record<string, unknown>`. That is too weak for capability-driven UI.
232
233Add a typed capability surface in `packages/contracts` that both server and web can import directly.
234
235Preferred 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
241Minimum fields:
242
243```ts
244interface ProviderInteractiveCapabilities {
245 supportsPlanMode: boolean;
246 supportsStructuredPlanUpdates: boolean;
247 supportsPlanTextStreaming: boolean;
248 supportsStructuredUserInput: boolean;
249 supportsFreeformUserInput: boolean;
250 supportsPlanAcceptance: boolean;
251}
252```
253
254Preferred 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
262Current shared commands only support approvals:
263
264- `thread.approval.respond`
265- provider `respondToRequest(...)`
266
267Plan mode needs a distinct path for structured prompt answers.
268
269Preferred 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
275Do not overload approval decisions like `accept` / `decline` for structured question answers.
276
277## 2. Extend the orchestration read model
278
279The current read model is the main missing piece.
280
281Plan 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
285Preferred thread-level additions:
286
287- `interactionMode`
288- `currentPlan`
289- `pendingUserInput`
290- `resolvedUserInputs`
291- optional `finalPlanArtifact`
292
293The 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
297Preferred 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
308This 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
323It should be extended to also translate plan/user-input runtime events into read-model state.
324
325Implementation 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
335The web architecture already has the right places.
336
337### Store and types
338
339Update:
340
341- `apps/web/src/types.ts`
342- `apps/web/src/store.ts`
343
344The store should stay thin and snapshot-oriented. It should sync whatever new read-model fields the server exposes.
345
346### Derived UI state
347
348Update:
349
350- `apps/web/src/session-logic.ts`
351
352This is the natural place for:
353
354- `derivePlanState(...)`
355- `derivePendingUserInput(...)`
356- any fallback derivation from activities during an incremental migration
357
358### Rendering
359
360Primary UI seam:
361
362- `apps/web/src/components/ChatView.tsx`
363
364Current 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
370That suggests two viable rendering patterns:
371
3721. active plan/prompt panel above the timeline
3732. resolved plan/prompt entries rendered in timeline/history
374
375The 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
383Codex is closest to the target shape and should be the first end-to-end implementation.
384
385### Current status
386
387Already present:
388
389- native structured plan update mapping
390- native plan-text stream mapping
391
392Missing:
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
4011. Upgrade `item/tool/requestUserInput` from request-only handling to canonical `user-input.requested`.
4022. Preserve `request.opened` as transport/debug metadata when useful.
4033. Add adapter-side answer submission for the corresponding pending request/tool context.
4044. Emit `user-input.resolved` when the answer is submitted or confirmed resolved.
4055. Project the result into the orchestration snapshot.
406
407### Codex-specific note
408
409The 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
413Claude likely needs tool-aware adaptation rather than pure transport mapping.
414
415### Current status
416
417Already present:
418
419- generic session/runtime stream integration
420- approval bridging
421
422Missing:
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
4301. Detect plan/default operating mode from Claude-native metadata without touching access-policy `runtimeMode`.
4312. Map `AskUserQuestion` tool payloads into canonical `user-input.requested`.
4323. Add provider-specific answer serialization for the return path.
4334. Detect `ExitPlanMode` and decide whether it becomes:
434 - a final-plan artifact
435 - a synthetic structured plan snapshot
436 - or both
4375. Avoid over-synthesizing incremental structured plan steps unless Claude gives us enough structure to do it safely.
438
439## Provider 3: Cursor
440
441Cursor should start from the same canonical server pipeline, but with a more conservative adapter.
442
443### Current status
444
445Already present:
446
447- session/update stream handling
448- reasoning/assistant text mapping
449- approval bridging
450
451Missing:
452
453- plan/default operating mode mapping
454- structured prompt mapping
455- structured plan/todo mapping
456
457### Required work
458
4591. Detect and publish Cursor operating mode separately from access-policy runtime mode.
4602. Map documented extension methods such as `cursor/ask_question` and `cursor/update_todos` only when payloads are confirmed.
4613. Fall back to plan-mode indicator plus normal assistant/reasoning text if structured Cursor plan events are absent.
4624. Preserve raw ACP payloads so the adapter can improve without changing higher layers.
463
464---
465
466## Frontend Rendering Plan
467
468The frontend should remain capability-driven, but using real repo seams.
469
470### Active state
471
472Render 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
482Render from:
483
484- normal messages
485- resolved prompt answers
486- plan-related activity/history rows
487- optional final-plan artifact
488
489### Placement
490
491Recommended initial placement:
492
4931. Active pending prompt and active plan panel above the timeline, near the current approvals stack.
4942. Resolved prompt/plan history in the timeline.
495
496This fits the existing `ChatView` layout with the least architectural churn.
497
498---
499
500## Test Plan
501
502### Contracts
503
504Add 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
511Retain 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
520Add 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
530Add 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
5441. Add shared contract types for interaction mode, capabilities, and structured-answer submission.
5452. Extend orchestration read-model contracts and projection storage for active plan/prompt state.
5463. Wire `ProviderRuntimeIngestion` into those projections.
5474. Finish Codex end-to-end first, since it already emits most of the canonical runtime pieces.
5485. Add frontend snapshot sync, derivation, and rendering in `types.ts`, `store.ts`, `session-logic.ts`, and `ChatView.tsx`.
5496. Add Claude adapter mapping.
5507. Add Cursor adapter mapping and fallback behavior.
551
552## Short version
553
554The repo is already close in one specific sense: the runtime event vocabulary for plans and structured prompts exists.
555
556The 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
564That is the shape the implementation should follow.