Skip to content

Commit 07edcf8

Browse files
committed
Add Claude Code provider integration
1 parent 105a167 commit 07edcf8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3680
-360
lines changed
Lines changed: 109 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,120 @@
1-
# Design Review: Claude Code Support
1+
# Claude Code Support — Execution Plan
22

3-
## Summary
3+
## Goal
44

5-
T3 Code is clearly being shaped toward multi-provider support, but the current implementation remains codex-first in both contracts and runtime assumptions. Claude Code support should land as a real provider adapter, not as a UI-only toggle.
5+
Ship `claudeCode` as a first-class provider in the current `apps/server` + `apps/web` stack, with predictable lifecycle behavior, canonical runtime events, and capability-driven UI gating.
66

7-
## What Already Helps
7+
## Architecture Fit
88

9-
- provider service / adapter architecture already exists under `apps/server/src/provider`
10-
- provider session directory and resume binding already exist
11-
- runtime events are normalized through canonical provider runtime ingestion
12-
- the web UI already gestures at unavailable providers in the picker
9+
This track is intentionally aligned to the current codebase, not legacy `apps/renderer` assumptions.
1310

14-
These are strong foundations.
11+
### Server runtime path
1512

16-
## Where Codex Still Leaks Through
17-
18-
### Contracts
19-
20-
`ProviderKind` is still effectively locked to Codex, so any provider abstraction above it is narrower than it appears.
13+
- `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
14+
- `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
15+
- `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`
16+
- `apps/server/src/provider/Layers/ProviderService.ts`
17+
- `apps/server/src/provider/Layers/ProviderSessionDirectory.ts`
18+
- `apps/server/src/provider/Layers/ProviderHealth.ts`
19+
- `apps/server/src/serverLayers.ts`
20+
- `apps/server/src/wsServer.ts`
2121

22-
Areas to widen first:
22+
### Shared contracts / model path
2323

2424
- `packages/contracts/src/orchestration.ts`
2525
- `packages/contracts/src/provider.ts`
26+
- `packages/contracts/src/providerRuntime.ts`
2627
- `packages/contracts/src/model.ts`
27-
28-
### Runtime protocol assumptions
29-
30-
Codex-specific semantics are still embedded in:
31-
32-
- `apps/server/src/codexAppServerManager.ts`
33-
- `apps/server/src/provider/Layers/CodexAdapter.ts`
34-
- collaboration mode / effort settings
35-
- model selection and account handling
36-
- resume semantics and turn lifecycle mapping
37-
38-
### UI assumptions
39-
40-
The UI already lists `claudeCode` as unavailable, but provider capabilities are not yet modeled deeply enough for different approval semantics, tool event shapes, or model option behavior.
41-
42-
## Recommended Direction
43-
44-
Treat Claude Code support as a canonical provider implementation with a first-class adapter and a capability matrix.
45-
46-
### Phase 1 — widen contracts
47-
48-
- expand `ProviderKind` to include `claudeCode`
49-
- add provider capability contracts:
50-
- model switch mode
51-
- approval support
52-
- user input support
53-
- collaboration / planning support
54-
- resume support level
55-
56-
### Phase 2 — add adapter
57-
58-
Introduce:
59-
60-
- `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
61-
- `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
62-
63-
This adapter should be responsible for:
64-
65-
- session startup
66-
- send turn
67-
- interrupt / stop
68-
- request/approval response mapping
69-
- event normalization into canonical `ProviderRuntimeEvent`
70-
71-
### Phase 3 — runtime normalization
72-
73-
Ensure `ProviderRuntimeIngestion` stays provider-agnostic by consuming canonical runtime events only.
74-
75-
If Claude requires provider-specific preprocessing, keep that inside the adapter layer.
76-
77-
### Phase 4 — UI enablement
78-
79-
- enable Claude in the provider picker only after the adapter is usable
80-
- gate unsupported features off capability flags, not hardcoded provider checks
81-
82-
## Capability Matrix to Add
83-
84-
Every provider should declare at least:
85-
86-
- `sessionModelSwitch`
87-
- `supportsApprovals`
88-
- `supportsUserInput`
89-
- `supportsResume`
90-
- `supportsCollaborationMode`
91-
- `supportsImageInputs`
92-
- `supportsReasoningEffort`
93-
- `supportsServiceTier`
94-
95-
This avoids future branching scattered across `ChatView.tsx`.
96-
97-
## Reentry Feature Implications
98-
99-
Claude support should plug into the proposed reentry engine in two ways:
100-
101-
1. as a runtime signal source through canonical provider events
102-
2. as an optional recap writer backend once the provider is stable
103-
104-
The recap system should not assume Codex-only event fields or model option semantics.
105-
106-
## Risks
107-
108-
- resume behavior may not match Codex thread recovery semantics
109-
- approval and tool event taxonomies may be materially different
110-
- collaboration mode may not have a direct Claude equivalent
111-
- provider-specific prompt tuning for recap generation could leak into server orchestration if not isolated
112-
113-
## Recommendation
114-
115-
Do **not** bolt Claude Code onto `CodexAppServerManager`. Instead:
116-
117-
- keep Codex-specific runtime logic in Codex modules
118-
- add provider capability contracts first
119-
- ship Claude through the existing adapter / registry / service architecture
120-
- keep recap generation behind a separate model broker so runtime provider and recap writer provider can differ
28+
- `packages/contracts/src/server.ts`
29+
- `packages/shared/src/model.ts`
30+
31+
### Web path
32+
33+
- `apps/web/src/components/ChatView.tsx`
34+
- `apps/web/src/composerDraftStore.ts`
35+
- `apps/web/src/store.ts`
36+
- `apps/web/src/session-logic.ts`
37+
- `apps/web/src/appSettings.ts`
38+
- `apps/web/src/routes/_chat.settings.tsx`
39+
- `apps/web/src/wsNativeApi.ts`
40+
41+
## Execution Tracks
42+
43+
### 1. Contracts and capability matrix
44+
45+
- Widen `ProviderKind` to include `claudeCode`.
46+
- Define provider capability contracts in `packages/contracts/src/provider.ts`:
47+
- `sessionModelSwitch`
48+
- `supportsApprovals`
49+
- `supportsUserInput`
50+
- `supportsResume`
51+
- `supportsCollaborationMode`
52+
- `supportsImageInputs`
53+
- `supportsReasoningEffort`
54+
- `supportsServiceTier`
55+
- `supportsConversationRollback`
56+
- Extend provider model options in `packages/contracts/src/model.ts`:
57+
- Codex: `reasoningEffort`, `fastMode`
58+
- Claude Code: `effort`
59+
- Extend runtime raw-source contracts in `packages/contracts/src/providerRuntime.ts` for Claude-native stream events.
60+
- Surface provider capabilities through `packages/contracts/src/server.ts` so `server.getConfig` becomes the single source of truth for provider availability + feature gating.
61+
62+
### 2. Server adapter and session lifecycle
63+
64+
- Add a dedicated `ClaudeCodeAdapter` under `apps/server/src/provider`.
65+
- Keep Codex-specific logic isolated in Codex modules.
66+
- Use the Claude runtime adapter to own:
67+
- session startup / resume
68+
- turn dispatch
69+
- interrupt / stop
70+
- canonical event emission
71+
- capability reporting
72+
- Preserve `ProviderService` as the cross-provider routing layer.
73+
- Preserve `ProviderSessionDirectory` as the persisted thread → provider binding / resume binding layer.
74+
- Register Claude in `ProviderAdapterRegistryLive` and `makeServerProviderLayer()`.
75+
76+
### 3. Health checks and WebSocket/API surface
77+
78+
- Extend `ProviderHealthLive` to probe Claude install/auth status alongside Codex.
79+
- Keep `server.getConfig` as the main transport surface for provider status + capability data.
80+
- Ensure `server.configUpdated` pushes continue to carry the latest provider status objects.
81+
- Do not add a parallel Claude-specific RPC surface unless the orchestration path cannot express a required operation.
82+
83+
### 4. Web provider parity
84+
85+
- Drive provider availability from `server.getConfig().providers`, not hardcoded placeholders.
86+
- Keep `PROVIDER_OPTIONS` as the UI label list, but compute selectable / unavailable providers from server status.
87+
- Extend settings to support custom Claude model slugs.
88+
- Extend the composer model / effort controls so they respect provider capabilities.
89+
- Gate unsupported features via capabilities instead of provider-name checks:
90+
- image inputs
91+
- plan/default interaction mode
92+
- service tier
93+
- conversation rollback
94+
- Keep event rendering provider-agnostic by consuming orchestration projections only.
95+
96+
### 5. Reliability requirements
97+
98+
- Resume / reconnect must rely on persisted provider session bindings, not fragile UI state.
99+
- Provider restarts must keep behavior deterministic:
100+
- no silent provider swapping
101+
- no hidden capability fallback without an explicit runtime warning
102+
- Partial stream handling must continue to produce stable timeline state if a turn is interrupted mid-stream.
103+
- Session stop / interrupt flows must settle orchestration thread state instead of leaving it ambiguous.
104+
105+
## Current implementation notes
106+
107+
This implementation track favors shared abstractions over one-off branching:
108+
109+
- provider capabilities are shared through contracts and reused by both server and web
110+
- provider status and capability data flow through `server.getConfig`
111+
- UI gating uses capability data instead of hardcoded `provider === "codex"` checks where possible
112+
- Claude support is added through the existing adapter / registry / service architecture instead of special-casing `wsServer`
113+
114+
## Follow-up work after this track
115+
116+
- Persist a thread-level preferred provider so idle threads do not need model-based provider inference.
117+
- Add richer Claude permission / elicitation bridging if the adapter surface stabilizes enough to expose it predictably.
118+
- Add Claude image-input support once the runtime path supports structured non-text turn inputs cleanly.
119+
- Revisit rollback / checkpoint parity if Claude exposes reversible conversation history primitives.
120+
- Add targeted integration tests for reconnect, restart, and interrupted partial-stream recovery on Claude sessions.

REMOTE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,23 @@ Open from any device in your tailnet:
6363
`http://<tailnet-ip>:3773`
6464

6565
You can also bind `--host 0.0.0.0` and connect through the Tailnet IP, but binding directly to the Tailnet IP limits exposure.
66+
67+
## 3) Tailnet access in `dev` / `dev-branch`
68+
69+
For the dev runner, prefer an explicit Tailnet host so the injected Vite URL, HMR socket, and app WebSocket URL all use the same remote-friendly address.
70+
71+
```bash
72+
T3CODE_HOST="$(tailscale ip -4)" \
73+
T3CODE_NO_BROWSER=1 \
74+
T3CODE_PORT_OFFSET=20 \
75+
T3CODE_STATE_DIR=~/.t3/dev-claude-branch \
76+
./scripts/dev-branch.sh
77+
```
78+
79+
Then open from another device in your tailnet:
80+
81+
`http://<tailnet-ip>:5753`
82+
83+
If you prefer to land on the server port first, open:
84+
85+
`http://<tailnet-ip>:3793`

apps/server/integration/TestProviderAdapter.integration.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
33
import {
44
ApprovalRequestId,
55
EventId,
6+
PROVIDER_CAPABILITIES_BY_PROVIDER,
67
ProviderApprovalDecision,
78
ProviderRuntimeEvent,
89
RuntimeSessionId,
@@ -35,7 +36,7 @@ export interface TestTurnResponse {
3536
export type FixtureProviderRuntimeEvent = {
3637
readonly type: string;
3738
readonly eventId: EventId;
38-
readonly provider: "codex";
39+
readonly provider: "codex" | "claudeCode";
3940
readonly createdAt: string;
4041
readonly threadId: string;
4142
readonly turnId?: string | undefined;
@@ -474,9 +475,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
474475

475476
const adapter: ProviderAdapterShape<ProviderAdapterError> = {
476477
provider,
477-
capabilities: {
478-
sessionModelSwitch: "in-session",
479-
},
478+
capabilities: PROVIDER_CAPABILITIES_BY_PROVIDER[provider],
480479
startSession,
481480
sendTurn,
482481
interruptTurn,

apps/server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
"test": "vitest run"
2323
},
2424
"dependencies": {
25+
"@anthropic-ai/claude-agent-sdk": "^0.2.71",
2526
"@effect/platform-node": "catalog:",
2627
"@effect/sql-sqlite-bun": "catalog:",
2728
"@pierre/diffs": "^1.1.0-beta.16",
2829
"effect": "catalog:",
2930
"node-pty": "^1.1.0",
3031
"open": "^10.1.0",
31-
"ws": "^8.18.0"
32+
"ws": "^8.18.0",
33+
"zod": "^4.3.6"
3234
},
3335
"devDependencies": {
3436
"@effect/language-service": "catalog:",

apps/server/src/main.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { Config, Data, Effect, FileSystem, Layer, Option, Path, Schema, ServiceMap } from "effect";
1010
import { Command, Flag } from "effect/unstable/cli";
1111
import { NetService } from "@t3tools/shared/Net";
12+
import { formatHostForUrl, isWildcardHost } from "@t3tools/shared/host";
1213
import {
1314
DEFAULT_PORT,
1415
resolveStaticDir,
@@ -204,12 +205,6 @@ const LayerLive = (input: CliInput) =>
204205
Layer.provideMerge(ServerConfigLive(input)),
205206
);
206207

207-
const isWildcardHost = (host: string | undefined): boolean =>
208-
host === "0.0.0.0" || host === "::" || host === "[::]";
209-
210-
const formatHostForUrl = (host: string): string =>
211-
host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
212-
213208
export const recordStartupHeartbeat = Effect.gen(function* () {
214209
const analytics = yield* AnalyticsService;
215210
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
EventId,
1111
MessageId,
1212
ProjectId,
13+
PROVIDER_CAPABILITIES_BY_PROVIDER,
1314
ThreadId,
1415
TurnId,
1516
} from "@t3tools/contracts";
@@ -89,7 +90,7 @@ function createProviderServiceHarness(
8990
respondToUserInput: () => unsupported(),
9091
stopSession: () => unsupported(),
9192
listSessions,
92-
getCapabilities: () => Effect.succeed({ sessionModelSwitch: "in-session" }),
93+
getCapabilities: () => Effect.succeed(PROVIDER_CAPABILITIES_BY_PROVIDER[providerName]),
9394
rollbackConversation,
9495
streamEvents: Stream.fromPubSub(runtimeEventPubSub),
9596
};

0 commit comments

Comments
 (0)