diff --git a/apps/server/package.json b/apps/server/package.json index 2d24a958bd680a141583f800780fe73e066c97d7..eb54ec15b449e96a59d9488c3a33ed3c2b69a4df 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -22,7 +22,6 @@ "test": "vitest run" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.62", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", "@pierre/diffs": "^1.1.0-beta.16", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 3394164d510b22607854ab0bd8bb7c7d1dc312fc..0b892472c7612760531dfd6db80c2fdbe85fb7e7 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -5279,6 +5279,7 @@ const MessagesTimeline = memo(function MessagesTimeline({ }); const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => option.available); +const UNAVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => !option.available); const COMING_SOON_PROVIDER_OPTIONS = [ { id: "opencode", label: "OpenCode", icon: OpenCodeIcon }, { id: "gemini", label: "Gemini", icon: Gemini }, @@ -5440,7 +5441,26 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { ); })} - + {UNAVAILABLE_PROVIDER_OPTIONS.length > 0 && } + {UNAVAILABLE_PROVIDER_OPTIONS.map((option) => { + const OptionIcon = PROVIDER_ICON_BY_PROVIDER[option.value]; + return ( + + + ); + })} + {UNAVAILABLE_PROVIDER_OPTIONS.length === 0 && } {COMING_SOON_PROVIDER_OPTIONS.map((option) => { const OptionIcon = option.icon; return ( diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index 144e209de4b228af2080e5f7cf4b6b95fa24a883..c814b796f4981f54672640c73f4a34739a9481a6 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -514,11 +514,23 @@ describe("isLatestTurnSettled", () => { }); describe("PROVIDER_OPTIONS", () => { - it("only advertises codex in the stack base", () => { + it("keeps Claude Code and Cursor visible as unavailable placeholders in the stack base", () => { const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode"); const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor"); - expect(PROVIDER_OPTIONS).toEqual([{ value: "codex", label: "Codex", available: true }]); - expect(claude).toBeUndefined(); - expect(cursor).toBeUndefined(); + expect(PROVIDER_OPTIONS).toEqual([ + { value: "codex", label: "Codex", available: true }, + { value: "claudeCode", label: "Claude Code", available: false }, + { value: "cursor", label: "Cursor", available: false }, + ]); + expect(claude).toEqual({ + value: "claudeCode", + label: "Claude Code", + available: false, + }); + expect(cursor).toEqual({ + value: "cursor", + label: "Cursor", + available: false, + }); }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index f41bf3d34bababa84f0865de7dac20e02c90d69b..adf0635f308b3162422de0d9d0bed4f363f362d5 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -16,6 +16,8 @@ export const PROVIDER_OPTIONS: Array<{ available: boolean; }> = [ { value: "codex", label: "Codex", available: true }, + { value: "claudeCode", label: "Claude Code", available: false }, + { value: "cursor", label: "Cursor", available: false }, ]; export interface WorkLogEntry { diff --git a/docs/plan-mode-deep-dive.md b/docs/plan-mode-deep-dive.md deleted file mode 100644 index 6088aa6c2a8d2e95e65202b07885b5797804c6a9..0000000000000000000000000000000000000000 --- a/docs/plan-mode-deep-dive.md +++ /dev/null @@ -1,564 +0,0 @@ -# Plan Mode Deep Dive - -## Goal - -Implement plan mode without fighting the architecture that already exists in this repo. - -The current app is: - -- provider-adapter driven on the server -- orchestration snapshot driven on the client -- already equipped with some plan-adjacent runtime events -- not yet equipped with first-class plan-mode state in the orchestration read model - -This document updates the implementation plan to match that reality. - ---- - -## Current Repo Baseline - -### 1. Shared contracts already have some plan primitives - -`packages/contracts/src/providerRuntime.ts` already includes: - -- `turn.plan.updated` -- `content.delta` with `streamKind: "plan_text"` -- `user-input.requested` -- `user-input.resolved` - -Important constraint: - -- these are runtime-event primitives only -- there is no first-class provider capability schema -- there is no first-class plan operating mode field -- there is no dedicated final-plan artifact schema - -### 2. Existing `runtimeMode` is not plan mode - -`RuntimeMode` in `packages/contracts/src/orchestration.ts` and `packages/contracts/src/provider.ts` means execution policy: - -- `approval-required` -- `full-access` - -Plan mode must not overload that field. - -If we need provider operating mode, it needs a separate concept and separate naming, for example: - -- `interactionMode` -- `planningMode` -- `providerOperatingMode` - -Anything is acceptable as long as it is not reused from the existing access-policy `runtimeMode`. - -### 3. Server flow is already canonical-event-first - -The current server pipeline is: - -1. Provider adapters emit `ProviderRuntimeEvent`s. -2. `ProviderService` multiplexes adapter streams and maintains provider session bindings. -3. `ProviderRuntimeIngestion` translates runtime events into orchestration commands and thread activities. -4. `ProjectionPipeline` persists read-model tables. -5. `ProjectionSnapshotQuery` rebuilds the orchestration snapshot returned by `orchestration.getSnapshot`. -6. `wsServer` exposes generic orchestration snapshot, command dispatch, diff, and replay methods. - -This matters because plan mode should fit into the same pipeline, not bypass it with a parallel websocket protocol. - -Important detail: - -- `wsServer` broadcasts orchestration domain events, not raw provider runtime events -- if the web should see plan state live, that state must first become orchestration events and/or projections - -### 4. The current read model has nowhere to put plan mode - -`OrchestrationThread` currently contains: - -- `messages` -- `activities` -- `checkpoints` -- `session` -- `latestTurn` -- thread metadata like title, model, branch, worktree - -It does not contain: - -- current structured plan snapshot -- accumulated plan-text stream -- pending structured user-input prompt -- resolved structured answers -- provider interactive capabilities -- provider plan/default operating mode -- final approved plan artifact - -Today the only flexible place plan data can survive into the web snapshot is `activities[].payload`. - -### 5. The web app is snapshot-driven, not event-reduced - -The client flow today is: - -1. `EventRouter` listens to `orchestration.domainEvent`. -2. On domain events, it re-fetches `orchestration.getSnapshot`. -3. Zustand stores the latest normalized snapshot. -4. `session-logic.ts` derives UI state like pending approvals and work log from thread activities. -5. `ChatView` renders the thread from snapshot state plus those derived selectors. - -Important implication: - -- plan mode should be added to the orchestration snapshot and/or activity derivation path -- it should not depend on a separate client-side event reducer - -### 6. Current implementation status by provider - -#### Codex - -Currently implemented: - -- `turn/plan/updated` maps to `turn.plan.updated` -- `item/plan/delta` maps to `content.delta` with `streamKind: "plan_text"` - -Current gap: - -- `item/tool/requestUserInput` is currently classified as `request.opened` with request type `tool_user_input` -- it is not yet upgraded into `user-input.requested` -- there is no structured answer submission path yet - -#### Claude Code - -Currently implemented: - -- session/config metadata surfaces through `session.configured` -- assistant/reasoning text flows through normal runtime content events -- approval requests already bridge into `request.opened` / `request.resolved` - -Current gap: - -- no current canonical plan-mode mapping -- no current `AskUserQuestion` mapping -- no current `ExitPlanMode` mapping - -#### Cursor - -Currently implemented: - -- `available_commands_update` maps to `session.configured` -- thought/message chunks map to `content.delta` -- permission requests bridge into approval request events - -Current gap: - -- no current plan-mode mapping -- no current structured prompt mapping -- no current todo/plan projection - ---- - -## Design Constraints - -### Keep the current transport model - -Plan mode should continue to flow through: - -- `ProviderRuntimeEvent` -- orchestration commands/events -- projection tables -- `orchestration.getSnapshot` -- `orchestration.domainEvent` - -Do not introduce provider-specific websocket channels unless the current architecture proves insufficient. - -### Keep the client snapshot-driven - -The client already assumes that server state is re-synced from snapshots. Plan mode should respect that. - -That means: - -- server-side persistence matters more than clever client buffering -- reconnect behavior should come from projection state, not ad hoc React state -- `session-logic.ts` and `ChatView` are the main frontend extension seams - -### Keep approvals separate from structured user input - -The current system has a full approval pipeline: - -- runtime `request.opened` / `request.resolved` -- orchestration `thread.approval.respond` -- provider `respondToRequest(...)` - -Plan-mode structured prompts are different and should not be forced into the approval model unless a provider genuinely models them as approval requests. - -### Preserve raw provider payloads - -When adding plan-mode mappings: - -- keep `raw` on internal canonical runtime events -- keep `providerRefs` on internal canonical runtime events -- avoid throwing away native payload shape too early inside adapter/server processing - -This does not mean provider-specific payloads should leak into the orchestration read model, websocket API, or web UI. - -The boundary should stay: - -- adapter/server internals may retain native payloads for correlation, debugging, and future parser improvements -- shared orchestration state and client-visible contracts should expose only canonical plan-mode data - -That matters most for Claude and Cursor, where some mappings will be adapter-derived and may need native context during implementation and debugging. - ---- - -## Updated Shared Work Plan - -## 1. Tighten shared contract naming before wiring features - -The first step is not to invent a new architecture. It is to formalize the concepts that are currently missing from contracts. - -### Add a plan operating mode concept - -Add a new shared contract for provider plan/default mode. - -Requirements: - -- do not reuse `RuntimeMode` -- keep it orthogonal to access policy -- make it available in the server->web read model, not only in raw runtime payloads - -Preferred shape: - -```ts -type ProviderInteractionMode = "default" | "plan"; -``` - -### Add typed provider interactive capabilities - -Current `session.configured.payload.config` is just `Record`. That is too weak for capability-driven UI. - -Add a typed capability surface in `packages/contracts` that both server and web can import directly. - -Preferred shape: - -- exported constant/object keyed by `ProviderKind` -- exported type derived from that object -- usable by adapters, orchestration code, and frontend rendering logic without duplicating provider capability tables - -Minimum fields: - -```ts -interface ProviderInteractiveCapabilities { - supportsPlanMode: boolean; - supportsStructuredPlanUpdates: boolean; - supportsPlanTextStreaming: boolean; - supportsStructuredUserInput: boolean; - supportsFreeformUserInput: boolean; - supportsPlanAcceptance: boolean; -} -``` - -Preferred usage: - -- static provider capabilities live in shared contracts -- adapters may still emit dynamic runtime metadata when a capability is session-specific or probe-dependent -- the orchestration snapshot should expose the active provider and any dynamic overrides, not duplicate the full static capability catalog per thread - -### Add a dedicated structured-answer command path - -Current shared commands only support approvals: - -- `thread.approval.respond` -- provider `respondToRequest(...)` - -Plan mode needs a distinct path for structured prompt answers. - -Preferred additions: - -- orchestration command such as `thread.user-input.respond` -- provider service method such as `respondToUserInput(...)` -- adapter hook per provider for native answer serialization - -Do not overload approval decisions like `accept` / `decline` for structured question answers. - -## 2. Extend the orchestration read model - -The current read model is the main missing piece. - -Plan mode needs first-class projection state so reconnect and refresh behave like the rest of the app. - -### Add first-class thread plan state - -Preferred thread-level additions: - -- `interactionMode` -- `currentPlan` -- `pendingUserInput` -- `resolvedUserInputs` -- optional `finalPlanArtifact` - -The exact nesting can vary, but it should be snapshot-friendly and not require replaying arbitrary activity payloads in the client. - -`interactiveCapabilities` should not be modeled as thread-owned state if they are static per provider. - -Preferred split: - -- static capabilities: shared contract export keyed by provider -- dynamic per-thread/per-session state: only fields that can actually vary at runtime, such as current interaction mode or active pending prompt - -### Keep activities as secondary audit trail - -`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. - -### Projection changes - -This likely means: - -- new projection repository/table(s) for plan state and prompt state -- `ProjectionPipeline` updates -- `ProjectionSnapshotQuery` returning the new fields -- projection tests for persistence and replay - -## 3. Update runtime ingestion to populate that state - -`ProviderRuntimeIngestion` already knows how to: - -- turn assistant text into messages -- turn approval events into activities -- turn `turn.plan.updated` into an activity - -It should be extended to also translate plan/user-input runtime events into read-model state. - -Implementation direction: - -- `turn.plan.updated` updates projected plan snapshot -- `content.delta(plan_text)` appends to projected plan-text buffer when relevant -- `user-input.requested` opens a projected pending prompt -- `user-input.resolved` closes/resolves that prompt -- providers that only expose raw/native blobs can still emit synthesized canonical runtime events first, then rely on the shared ingestion path - -## 4. Integrate plan mode into the existing web seams - -The web architecture already has the right places. - -### Store and types - -Update: - -- `apps/web/src/types.ts` -- `apps/web/src/store.ts` - -The store should stay thin and snapshot-oriented. It should sync whatever new read-model fields the server exposes. - -### Derived UI state - -Update: - -- `apps/web/src/session-logic.ts` - -This is the natural place for: - -- `derivePlanState(...)` -- `derivePendingUserInput(...)` -- any fallback derivation from activities during an incremental migration - -### Rendering - -Primary UI seam: - -- `apps/web/src/components/ChatView.tsx` - -Current UI already has: - -- top-of-thread alert stacks for pending approvals -- work-log rendering derived from activities -- timeline rows for messages and work state - -That suggests two viable rendering patterns: - -1. active plan/prompt panel above the timeline -2. resolved plan/prompt entries rendered in timeline/history - -The route structure does not need to change. Plan mode should remain thread-local state inside the existing chat route. - ---- - -## Provider Implementation Plan - -## Provider 1: Codex - -Codex is closest to the target shape and should be the first end-to-end implementation. - -### Current status - -Already present: - -- native structured plan update mapping -- native plan-text stream mapping - -Missing: - -- native structured prompt mapping for `item/tool/requestUserInput` -- structured answer submission path -- removal of the current empty-answer auto-ack path in `codexAppServerManager` -- projection and UI support for the already-emitted plan events - -### Required work - -1. Upgrade `item/tool/requestUserInput` from request-only handling to canonical `user-input.requested`. -2. Preserve `request.opened` as transport/debug metadata when useful. -3. Add adapter-side answer submission for the corresponding pending request/tool context. -4. Emit `user-input.resolved` when the answer is submitted or confirmed resolved. -5. Project the result into the orchestration snapshot. - -### Codex-specific note - -The runtime event schemas already match Codex reasonably well, so this provider should define the canonical end-to-end behavior first. - -## Provider 2: Claude Code - -Claude likely needs tool-aware adaptation rather than pure transport mapping. - -### Current status - -Already present: - -- generic session/runtime stream integration -- approval bridging - -Missing: - -- plan/default operating mode mapping -- `AskUserQuestion` -> `user-input.requested` -- `ExitPlanMode` -> final-plan or plan-state handoff - -### Required work - -1. Detect plan/default operating mode from Claude-native metadata without touching access-policy `runtimeMode`. -2. Map `AskUserQuestion` tool payloads into canonical `user-input.requested`. -3. Add provider-specific answer serialization for the return path. -4. Detect `ExitPlanMode` and decide whether it becomes: - - a final-plan artifact - - a synthetic structured plan snapshot - - or both -5. Avoid over-synthesizing incremental structured plan steps unless Claude gives us enough structure to do it safely. - -## Provider 3: Cursor - -Cursor should start from the same canonical server pipeline, but with a more conservative adapter. - -### Current status - -Already present: - -- session/update stream handling -- reasoning/assistant text mapping -- approval bridging - -Missing: - -- plan/default operating mode mapping -- structured prompt mapping -- structured plan/todo mapping - -### Required work - -1. Detect and publish Cursor operating mode separately from access-policy runtime mode. -2. Map documented extension methods such as `cursor/ask_question` and `cursor/update_todos` only when payloads are confirmed. -3. Fall back to plan-mode indicator plus normal assistant/reasoning text if structured Cursor plan events are absent. -4. Preserve raw ACP payloads so the adapter can improve without changing higher layers. - ---- - -## Frontend Rendering Plan - -The frontend should remain capability-driven, but using real repo seams. - -### Active state - -Render from projected snapshot fields: - -- current plan/default operating mode -- provider capabilities -- current structured plan -- current plan-text stream -- pending structured prompt - -### History state - -Render from: - -- normal messages -- resolved prompt answers -- plan-related activity/history rows -- optional final-plan artifact - -### Placement - -Recommended initial placement: - -1. Active pending prompt and active plan panel above the timeline, near the current approvals stack. -2. Resolved prompt/plan history in the timeline. - -This fits the existing `ChatView` layout with the least architectural churn. - ---- - -## Test Plan - -### Contracts - -Add coverage for: - -- new interaction-mode schema -- new capability schema -- new structured-answer command schema -- any read-model additions for active plan/prompt state - -Retain existing coverage for: - -- `turn.plan.updated` -- `content.delta(plan_text)` -- `user-input.requested` -- `user-input.resolved` - -### Server - -Add or extend tests for: - -- provider adapter plan/prompt mapping -- `ProviderRuntimeIngestion` plan/prompt projection behavior -- `ProjectionPipeline` persistence and replay -- `ProjectionSnapshotQuery` returning active plan state -- provider answer submission path - -### Web - -Add or extend tests for: - -- `syncServerReadModel` with new plan fields -- `session-logic.ts` derivation of active plan/prompt state -- `ChatView` rendering of: - - active plan panel - - pending structured prompt - - resolved prompt history - - plan-text fallback - ---- - -## Recommended Execution Order - -1. Add shared contract types for interaction mode, capabilities, and structured-answer submission. -2. Extend orchestration read-model contracts and projection storage for active plan/prompt state. -3. Wire `ProviderRuntimeIngestion` into those projections. -4. Finish Codex end-to-end first, since it already emits most of the canonical runtime pieces. -5. Add frontend snapshot sync, derivation, and rendering in `types.ts`, `store.ts`, `session-logic.ts`, and `ChatView.tsx`. -6. Add Claude adapter mapping. -7. Add Cursor adapter mapping and fallback behavior. - -## Short version - -The repo is already close in one specific sense: the runtime event vocabulary for plans and structured prompts exists. - -The real missing architecture is: - -- a separate plan/default operating mode concept -- typed provider capabilities -- a first-class structured-answer command path -- read-model projection for active plan/prompt state -- web rendering sourced from that snapshot state - -That is the shape the implementation should follow.