authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 21:27:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log6496aedc92513fe138a511c7afb9f51152d08e88
tree37f8e5fad9adcfab4a1ad169063c368c5cde5301
parente972fad30075aeff9831f38052ef05badabd236f

split: move Claude provider surface to sibling stack

Co-authored-by: codex <codex@users.noreply.github.com>

23 files changed, 48 insertions(+), 506 deletions(-)

apps/server/integration/OrchestrationEngineHarness.integration.ts+1-1
......@@ -188,7 +188,7 @@ export interface OrchestrationIntegrationHarness {
188188}
189189
190190interface MakeOrchestrationIntegrationHarnessOptions {
191 readonly provider?: "codex" | "claudeCode";
191 readonly provider?: "codex" | "cursor";
192192 readonly realCodex?: boolean;
193193}
194194
apps/server/integration/TestProviderAdapter.integration.ts+5-5
......@@ -35,7 +35,7 @@ export interface TestTurnResponse {
3535export type FixtureProviderRuntimeEvent = {
3636 readonly type: string;
3737 readonly eventId: EventId;
38 readonly provider: "codex" | "claudeCode" | "cursor";
38 readonly provider: "codex" | "cursor";
3939 readonly createdAt: string;
4040 readonly threadId: string;
4141 readonly turnId?: string | undefined;
......@@ -178,7 +178,7 @@ function normalizeFixtureEvent(rawEvent: Record<string, unknown>): ProviderRunti
178178
179179export interface TestProviderAdapterHarness {
180180 readonly adapter: ProviderAdapterShape<ProviderAdapterError>;
181 readonly provider: "codex" | "claudeCode";
181 readonly provider: "codex" | "cursor";
182182 readonly queueTurnResponse: (
183183 threadId: ThreadId,
184184 response: TestTurnResponse,
......@@ -198,7 +198,7 @@ export interface TestProviderAdapterHarness {
198198}
199199
200200interface MakeTestProviderAdapterHarnessOptions {
201 readonly provider?: "codex" | "claudeCode";
201 readonly provider?: "codex" | "cursor";
202202}
203203
204204function nowIso(): string {
......@@ -206,7 +206,7 @@ function nowIso(): string {
206206}
207207
208208function sessionNotFound(
209 provider: "codex" | "claudeCode",
209 provider: "codex" | "cursor",
210210 threadId: ThreadId,
211211): ProviderAdapterSessionNotFoundError {
212212 return new ProviderAdapterSessionNotFoundError({
......@@ -216,7 +216,7 @@ function sessionNotFound(
216216}
217217
218218function missingSessionEffect(
219 provider: "codex" | "claudeCode",
219 provider: "codex" | "cursor",
220220 threadId: ThreadId,
221221): Effect.Effect<never, ProviderAdapterError> {
222222 return Effect.fail(sessionNotFound(provider, threadId));
apps/server/integration/orchestrationEngine.integration.test.ts+1-414
......@@ -32,7 +32,7 @@ const PROJECT_ID = asProjectId("project-1");
3232const THREAD_ID = ThreadId.makeUnsafe("thread-1");
3333const FIXTURE_TURN_ID = "fixture-turn";
3434const APPROVAL_REQUEST_ID = asApprovalRequestId("req-approval-1");
35type IntegrationProvider = "codex" | "claudeCode";
35type IntegrationProvider = "codex" | "cursor";
3636
3737function nowIso() {
3838 return new Date().toISOString();
......@@ -842,416 +842,3 @@ it.live(
842842 }),
843843 ),
844844);
845
846it.live("starts a claudeCode session on first turn when provider is requested", () =>
847 withHarness(
848 (harness) =>
849 Effect.gen(function* () {
850 yield* seedProjectAndThread(harness);
851
852 yield* harness.adapterHarness.queueTurnResponseForNextSession({
853 events: [
854 {
855 type: "turn.started",
856 ...runtimeBase("evt-claude-start-1", "2026-02-24T10:10:00.000Z", "claudeCode"),
857 threadId: THREAD_ID,
858 turnId: FIXTURE_TURN_ID,
859 },
860 {
861 type: "message.delta",
862 ...runtimeBase("evt-claude-start-2", "2026-02-24T10:10:00.050Z", "claudeCode"),
863 threadId: THREAD_ID,
864 turnId: FIXTURE_TURN_ID,
865 delta: "Claude first turn.\n",
866 },
867 {
868 type: "turn.completed",
869 ...runtimeBase("evt-claude-start-3", "2026-02-24T10:10:00.100Z", "claudeCode"),
870 threadId: THREAD_ID,
871 turnId: FIXTURE_TURN_ID,
872 status: "completed",
873 },
874 ],
875 });
876
877 yield* startTurn({
878 harness,
879 commandId: "cmd-turn-start-claude-initial",
880 messageId: "msg-user-claude-initial",
881 text: "Use Claude",
882 provider: "claudeCode",
883 });
884
885 const thread = yield* harness.waitForThread(
886 THREAD_ID,
887 (entry) =>
888 entry.session?.providerName === "claudeCode" &&
889 entry.session.status === "ready" &&
890 entry.messages.some(
891 (message) => message.role === "assistant" && message.text === "Claude first turn.\n",
892 ),
893 );
894 assert.equal(thread.session?.providerName, "claudeCode");
895 }),
896 "claudeCode",
897 ),
898);
899
900it.live("recovers claudeCode sessions after provider stopAll using persisted resume state", () =>
901 withHarness(
902 (harness) =>
903 Effect.gen(function* () {
904 yield* seedProjectAndThread(harness);
905
906 yield* harness.adapterHarness.queueTurnResponseForNextSession({
907 events: [
908 {
909 type: "turn.started",
910 ...runtimeBase("evt-claude-recover-1", "2026-02-24T10:11:00.000Z", "claudeCode"),
911 threadId: THREAD_ID,
912 turnId: FIXTURE_TURN_ID,
913 },
914 {
915 type: "message.delta",
916 ...runtimeBase("evt-claude-recover-2", "2026-02-24T10:11:00.050Z", "claudeCode"),
917 threadId: THREAD_ID,
918 turnId: FIXTURE_TURN_ID,
919 delta: "Turn before restart.\n",
920 },
921 {
922 type: "turn.completed",
923 ...runtimeBase("evt-claude-recover-3", "2026-02-24T10:11:00.100Z", "claudeCode"),
924 threadId: THREAD_ID,
925 turnId: FIXTURE_TURN_ID,
926 status: "completed",
927 },
928 ],
929 });
930
931 yield* startTurn({
932 harness,
933 commandId: "cmd-turn-start-claude-recover-1",
934 messageId: "msg-user-claude-recover-1",
935 text: "Before restart",
936 provider: "claudeCode",
937 });
938
939 yield* harness.waitForThread(
940 THREAD_ID,
941 (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1",
942 );
943
944 yield* harness.providerService.stopAll();
945 yield* waitForSync(
946 () => harness.adapterHarness.listActiveSessionIds(),
947 (sessionIds) => sessionIds.length === 0,
948 "provider stopAll",
949 );
950
951 yield* harness.adapterHarness.queueTurnResponseForNextSession({
952 events: [
953 {
954 type: "turn.started",
955 ...runtimeBase("evt-claude-recover-4", "2026-02-24T10:11:01.000Z", "claudeCode"),
956 threadId: THREAD_ID,
957 turnId: FIXTURE_TURN_ID,
958 },
959 {
960 type: "message.delta",
961 ...runtimeBase("evt-claude-recover-5", "2026-02-24T10:11:01.050Z", "claudeCode"),
962 threadId: THREAD_ID,
963 turnId: FIXTURE_TURN_ID,
964 delta: "Turn after restart.\n",
965 },
966 {
967 type: "turn.completed",
968 ...runtimeBase("evt-claude-recover-6", "2026-02-24T10:11:01.100Z", "claudeCode"),
969 threadId: THREAD_ID,
970 turnId: FIXTURE_TURN_ID,
971 status: "completed",
972 },
973 ],
974 });
975
976 yield* startTurn({
977 harness,
978 commandId: "cmd-turn-start-claude-recover-2",
979 messageId: "msg-user-claude-recover-2",
980 text: "After restart",
981 });
982 yield* waitForSync(
983 () => harness.adapterHarness.getStartCount(),
984 (count) => count === 2,
985 "claude provider recovery start",
986 );
987
988 const recoveredThread = yield* harness.waitForThread(
989 THREAD_ID,
990 (entry) =>
991 entry.session?.providerName === "claudeCode" &&
992 entry.messages.some(
993 (message) => message.role === "user" && message.text === "After restart",
994 ) &&
995 !entry.activities.some((activity) => activity.kind === "provider.turn.start.failed"),
996 );
997 assert.equal(recoveredThread.session?.providerName, "claudeCode");
998 assert.equal(recoveredThread.session?.threadId, "thread-1");
999 }),
1000 "claudeCode",
1001 ),
1002);
1003
1004it.live("forwards claudeCode approval responses to the provider session", () =>
1005 withHarness(
1006 (harness) =>
1007 Effect.gen(function* () {
1008 yield* seedProjectAndThread(harness);
1009
1010 yield* harness.adapterHarness.queueTurnResponseForNextSession({
1011 events: [
1012 {
1013 type: "turn.started",
1014 ...runtimeBase("evt-claude-approval-1", "2026-02-24T10:12:00.000Z", "claudeCode"),
1015 threadId: THREAD_ID,
1016 turnId: FIXTURE_TURN_ID,
1017 },
1018 {
1019 type: "approval.requested",
1020 ...runtimeBase("evt-claude-approval-2", "2026-02-24T10:12:00.050Z", "claudeCode"),
1021 threadId: THREAD_ID,
1022 turnId: FIXTURE_TURN_ID,
1023 requestId: APPROVAL_REQUEST_ID,
1024 requestKind: "command",
1025 detail: "Approve Claude tool call",
1026 },
1027 {
1028 type: "turn.completed",
1029 ...runtimeBase("evt-claude-approval-3", "2026-02-24T10:12:00.100Z", "claudeCode"),
1030 threadId: THREAD_ID,
1031 turnId: FIXTURE_TURN_ID,
1032 status: "completed",
1033 },
1034 ],
1035 });
1036
1037 yield* startTurn({
1038 harness,
1039 commandId: "cmd-turn-start-claude-approval",
1040 messageId: "msg-user-claude-approval",
1041 text: "Need approval",
1042 provider: "claudeCode",
1043 });
1044
1045 const thread = yield* harness.waitForThread(THREAD_ID, (entry) =>
1046 entry.activities.some((activity) => activity.kind === "approval.requested"),
1047 );
1048 assert.equal(thread.session?.threadId, "thread-1");
1049
1050 yield* harness.engine.dispatch({
1051 type: "thread.approval.respond",
1052 commandId: CommandId.makeUnsafe("cmd-claude-approval-respond"),
1053 threadId: THREAD_ID,
1054 requestId: APPROVAL_REQUEST_ID,
1055 decision: "accept",
1056 createdAt: nowIso(),
1057 });
1058
1059 yield* harness.waitForPendingApproval(
1060 "req-approval-1",
1061 (row) => row.status === "resolved" && row.decision === "accept",
1062 );
1063
1064 const approvalResponses = yield* waitForSync(
1065 () => harness.adapterHarness.getApprovalResponses(THREAD_ID),
1066 (responses) => responses.length === 1,
1067 "claude provider approval response",
1068 );
1069 assert.equal(approvalResponses[0]?.decision, "accept");
1070 }),
1071 "claudeCode",
1072 ),
1073);
1074
1075it.live("forwards thread.turn.interrupt to claudeCode provider sessions", () =>
1076 withHarness(
1077 (harness) =>
1078 Effect.gen(function* () {
1079 yield* seedProjectAndThread(harness);
1080
1081 yield* harness.adapterHarness.queueTurnResponseForNextSession({
1082 events: [
1083 {
1084 type: "turn.started",
1085 ...runtimeBase("evt-claude-interrupt-1", "2026-02-24T10:13:00.000Z", "claudeCode"),
1086 threadId: THREAD_ID,
1087 turnId: FIXTURE_TURN_ID,
1088 },
1089 {
1090 type: "message.delta",
1091 ...runtimeBase("evt-claude-interrupt-2", "2026-02-24T10:13:00.050Z", "claudeCode"),
1092 threadId: THREAD_ID,
1093 turnId: FIXTURE_TURN_ID,
1094 delta: "Long running output.\n",
1095 },
1096 {
1097 type: "turn.completed",
1098 ...runtimeBase("evt-claude-interrupt-3", "2026-02-24T10:13:00.100Z", "claudeCode"),
1099 threadId: THREAD_ID,
1100 turnId: FIXTURE_TURN_ID,
1101 status: "completed",
1102 },
1103 ],
1104 });
1105
1106 yield* startTurn({
1107 harness,
1108 commandId: "cmd-turn-start-claude-interrupt",
1109 messageId: "msg-user-claude-interrupt",
1110 text: "Start long turn",
1111 provider: "claudeCode",
1112 });
1113
1114 const thread = yield* harness.waitForThread(
1115 THREAD_ID,
1116 (entry) => entry.session?.threadId === "thread-1",
1117 );
1118 assert.equal(thread.session?.threadId, "thread-1");
1119
1120 yield* harness.engine.dispatch({
1121 type: "thread.turn.interrupt",
1122 commandId: CommandId.makeUnsafe("cmd-turn-interrupt-claude"),
1123 threadId: THREAD_ID,
1124 createdAt: nowIso(),
1125 });
1126 yield* harness.waitForDomainEvent((event) => event.type === "thread.turn-interrupt-requested");
1127
1128 const interruptCalls = yield* waitForSync(
1129 () => harness.adapterHarness.getInterruptCalls(THREAD_ID),
1130 (calls) => calls.length === 1,
1131 "claude provider interrupt call",
1132 );
1133 assert.equal(interruptCalls.length, 1);
1134 }),
1135 "claudeCode",
1136 ),
1137);
1138
1139it.live("reverts claudeCode turns and rolls back provider conversation state", () =>
1140 withHarness(
1141 (harness) =>
1142 Effect.gen(function* () {
1143 yield* seedProjectAndThread(harness);
1144
1145 yield* harness.adapterHarness.queueTurnResponseForNextSession({
1146 events: [
1147 {
1148 type: "turn.started",
1149 ...runtimeBase("evt-claude-revert-1", "2026-02-24T10:14:00.000Z", "claudeCode"),
1150 threadId: THREAD_ID,
1151 turnId: FIXTURE_TURN_ID,
1152 },
1153 {
1154 type: "message.delta",
1155 ...runtimeBase("evt-claude-revert-2", "2026-02-24T10:14:00.050Z", "claudeCode"),
1156 threadId: THREAD_ID,
1157 turnId: FIXTURE_TURN_ID,
1158 delta: "README -> v2\n",
1159 },
1160 {
1161 type: "turn.completed",
1162 ...runtimeBase("evt-claude-revert-3", "2026-02-24T10:14:00.100Z", "claudeCode"),
1163 threadId: THREAD_ID,
1164 turnId: FIXTURE_TURN_ID,
1165 status: "completed",
1166 },
1167 ],
1168 mutateWorkspace: ({ cwd }) =>
1169 Effect.sync(() => {
1170 fs.writeFileSync(path.join(cwd, "README.md"), "v2\n", "utf8");
1171 }),
1172 });
1173
1174 yield* startTurn({
1175 harness,
1176 commandId: "cmd-turn-start-claude-revert-1",
1177 messageId: "msg-user-claude-revert-1",
1178 text: "First Claude edit",
1179 provider: "claudeCode",
1180 });
1181
1182 yield* harness.waitForThread(
1183 THREAD_ID,
1184 (entry) => entry.latestTurn?.turnId === "turn-1" && entry.session?.threadId === "thread-1",
1185 );
1186
1187 yield* harness.adapterHarness.queueTurnResponse(THREAD_ID, {
1188 events: [
1189 {
1190 type: "turn.started",
1191 ...runtimeBase("evt-claude-revert-4", "2026-02-24T10:14:01.000Z", "claudeCode"),
1192 threadId: THREAD_ID,
1193 turnId: FIXTURE_TURN_ID,
1194 },
1195 {
1196 type: "message.delta",
1197 ...runtimeBase("evt-claude-revert-5", "2026-02-24T10:14:01.050Z", "claudeCode"),
1198 threadId: THREAD_ID,
1199 turnId: FIXTURE_TURN_ID,
1200 delta: "README -> v3\n",
1201 },
1202 {
1203 type: "turn.completed",
1204 ...runtimeBase("evt-claude-revert-6", "2026-02-24T10:14:01.100Z", "claudeCode"),
1205 threadId: THREAD_ID,
1206 turnId: FIXTURE_TURN_ID,
1207 status: "completed",
1208 },
1209 ],
1210 mutateWorkspace: ({ cwd }) =>
1211 Effect.sync(() => {
1212 fs.writeFileSync(path.join(cwd, "README.md"), "v3\n", "utf8");
1213 }),
1214 });
1215
1216 yield* startTurn({
1217 harness,
1218 commandId: "cmd-turn-start-claude-revert-2",
1219 messageId: "msg-user-claude-revert-2",
1220 text: "Second Claude edit",
1221 });
1222
1223 yield* harness.waitForThread(
1224 THREAD_ID,
1225 (entry) =>
1226 entry.latestTurn?.turnId === "turn-2" &&
1227 entry.checkpoints.length === 2 &&
1228 entry.session?.providerName === "claudeCode",
1229 );
1230
1231 yield* harness.engine.dispatch({
1232 type: "thread.checkpoint.revert",
1233 commandId: CommandId.makeUnsafe("cmd-checkpoint-revert-claude"),
1234 threadId: THREAD_ID,
1235 turnCount: 1,
1236 createdAt: nowIso(),
1237 });
1238
1239 const revertedThread = yield* harness.waitForThread(
1240 THREAD_ID,
1241 (entry) =>
1242 entry.checkpoints.length === 1 && entry.checkpoints[0]?.checkpointTurnCount === 1,
1243 );
1244 assert.equal(revertedThread.checkpoints[0]?.checkpointTurnCount, 1);
1245 assert.equal(
1246 gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 1)),
1247 true,
1248 );
1249 assert.equal(
1250 gitRefExists(harness.workspaceDir, checkpointRefForThreadTurn(THREAD_ID, 2)),
1251 false,
1252 );
1253 assert.deepEqual(harness.adapterHarness.getRollbackCalls(THREAD_ID), [1]);
1254 }),
1255 "claudeCode",
1256 ),
1257);
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+1-1
......@@ -43,7 +43,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
4343type LegacyProviderRuntimeEvent = {
4444 readonly type: string;
4545 readonly eventId: EventId;
46 readonly provider: "codex" | "claudeCode" | "cursor";
46 readonly provider: "codex" | "cursor";
4747 readonly createdAt: string;
4848 readonly threadId: ThreadId;
4949 readonly turnId?: string | undefined;
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts-1
......@@ -212,7 +212,6 @@ const make = Effect.gen(function* () {
212212 const desiredRuntimeMode = thread.runtimeMode;
213213 const currentProvider: ProviderKind | undefined =
214214 thread.session?.providerName === "codex" ||
215 thread.session?.providerName === "claudeCode" ||
216215 thread.session?.providerName === "cursor"
217216 ? thread.session.providerName
218217 : undefined;
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+1-1
......@@ -41,7 +41,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
4141type LegacyProviderRuntimeEvent = {
4242 readonly type: string;
4343 readonly eventId: EventId;
44 readonly provider: "codex" | "claudeCode" | "cursor";
44 readonly provider: "codex" | "cursor";
4545 readonly createdAt: string;
4646 readonly threadId: ThreadId;
4747 readonly turnId?: string | undefined;
apps/server/src/provider/Layers/CodexAdapter.test.ts+2-2
......@@ -162,7 +162,7 @@ validationLayer("CodexAdapterLive validation", (it) => {
162162 const adapter = yield* CodexAdapter;
163163 const result = yield* adapter
164164 .startSession({
165 provider: "claudeCode",
165 provider: "cursor",
166166 threadId: asThreadId("thread-1"),
167167 runtimeMode: "full-access",
168168 })
......@@ -174,7 +174,7 @@ validationLayer("CodexAdapterLive validation", (it) => {
174174 new ProviderAdapterValidationError({
175175 provider: "codex",
176176 operation: "startSession",
177 issue: "Expected provider 'codex' but received 'claudeCode'.",
177 issue: "Expected provider 'codex' but received 'cursor'.",
178178 }),
179179 );
180180 assert.equal(validationManager.startSessionImpl.mock.calls.length, 0);
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+1-1
......@@ -1,7 +1,7 @@
11/**
22 * ProviderAdapterRegistryLive - In-memory provider adapter lookup layer.
33 *
4 * Binds provider kinds (codex/claudeCode/...) to concrete adapter services.
4 * Binds provider kinds (codex/cursor/...) to concrete adapter services.
55 * This layer only performs adapter lookup; it does not route session-scoped
66 * calls or own provider lifecycle workflows.
77 *
apps/server/src/provider/Layers/ProviderService.test.ts+1-1
......@@ -51,7 +51,7 @@ const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
5151type LegacyProviderRuntimeEvent = {
5252 readonly type: string;
5353 readonly eventId: EventId;
54 readonly provider: "codex" | "claudeCode" | "cursor";
54 readonly provider: "codex" | "cursor";
5555 readonly createdAt: string;
5656 readonly threadId: ThreadId;
5757 readonly turnId?: string | undefined;
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+1-1
......@@ -25,7 +25,7 @@ function decodeProviderKind(
2525 providerName: string,
2626 operation: string,
2727): Effect.Effect<ProviderKind, ProviderSessionDirectoryPersistenceError> {
28 if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") {
28 if (providerName === "codex" || providerName === "cursor") {
2929 return Effect.succeed(providerName);
3030 }
3131 return Effect.fail(
apps/web/src/appSettings.test.ts+3-9
......@@ -16,11 +16,7 @@ describe("normalizeCustomModelSlugs", () => {
1616 ).toEqual(["custom/internal-model"]);
1717 });
1818
19 it("normalizes provider-specific aliases for claude and cursor", () => {
20 expect(normalizeCustomModelSlugs(["sonnet"], "claudeCode")).toEqual([]);
21 expect(normalizeCustomModelSlugs(["claude/custom-sonnet"], "claudeCode")).toEqual([
22 "claude/custom-sonnet",
23 ]);
19 it("normalizes provider-specific aliases for cursor", () => {
2420 expect(normalizeCustomModelSlugs(["composer"], "cursor")).toEqual([]);
2521 expect(normalizeCustomModelSlugs(["cursor/custom-model"], "cursor")).toEqual([
2622 "cursor/custom-model",
......@@ -53,9 +49,9 @@ describe("getAppModelOptions", () => {
5349 });
5450
5551 it("keeps a saved custom provider model available as an exact slug option", () => {
56 const options = getAppModelOptions("claudeCode", ["claude/custom-opus"], "claude/custom-opus");
52 const options = getAppModelOptions("cursor", ["cursor/custom-model"], "cursor/custom-model");
5753
58 expect(options.some((option) => option.slug === "claude/custom-opus" && option.isCustom)).toBe(
54 expect(options.some((option) => option.slug === "cursor/custom-model" && option.isCustom)).toBe(
5955 true,
6056 );
6157 });
......@@ -85,10 +81,8 @@ describe("getSlashModelOptions", () => {
8581 });
8682
8783 it("includes provider-specific custom slugs in non-codex model lists", () => {
88 const claudeOptions = getAppModelOptions("claudeCode", ["claude/custom-opus"]);
8984 const cursorOptions = getAppModelOptions("cursor", ["cursor/custom-model"]);
9085
91 expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);
9286 expect(cursorOptions.some((option) => option.slug === "cursor/custom-model")).toBe(true);
9387 });
9488});
apps/web/src/appSettings.ts-5
......@@ -8,7 +8,6 @@ const MAX_CUSTOM_MODEL_COUNT = 32;
88export const MAX_CUSTOM_MODEL_LENGTH = 256;
99const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {
1010 codex: new Set(getModelOptions("codex").map((option) => option.slug)),
11 claudeCode: new Set(getModelOptions("claudeCode").map((option) => option.slug)),
1211 cursor: new Set(getModelOptions("cursor").map((option) => option.slug)),
1312};
1413
......@@ -26,9 +25,6 @@ const AppSettingsSchema = Schema.Struct({
2625 customCodexModels: Schema.Array(Schema.String).pipe(
2726 Schema.withConstructorDefault(() => Option.some([])),
2827 ),
29 customClaudeModels: Schema.Array(Schema.String).pipe(
30 Schema.withConstructorDefault(() => Option.some([])),
31 ),
3228 customCursorModels: Schema.Array(Schema.String).pipe(
3329 Schema.withConstructorDefault(() => Option.some([])),
3430 ),
......@@ -79,7 +75,6 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
7975 return {
8076 ...settings,
8177 customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),
82 customClaudeModels: normalizeCustomModelSlugs(settings.customClaudeModels, "claudeCode"),
8378 customCursorModels: normalizeCustomModelSlugs(settings.customCursorModels, "cursor"),
8479 };
8580}
apps/web/src/components/ChatView.tsx+16-11
......@@ -77,6 +77,7 @@ import {
7777 findLatestProposedPlan,
7878 type PendingApproval,
7979 type PendingUserInput,
80 type ProviderPickerKind,
8081 PROVIDER_OPTIONS,
8182 deriveWorkLogEntries,
8283 hasToolActivityForTurn,
......@@ -847,9 +848,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
847848 }, [modelOptionsByProvider, selectedModelForPicker, selectedProvider]);
848849 const searchableModelOptions = useMemo(
849850 () =>
850 PROVIDER_OPTIONS.filter(
851 (option) =>
852 option.available && (lockedProvider === null || option.value === lockedProvider),
851 AVAILABLE_PROVIDER_OPTIONS.filter(
852 (option) => lockedProvider === null || option.value === lockedProvider,
853853 ).flatMap((option) =>
854854 modelOptionsByProvider[option.value].map(({ slug, name }) => ({
855855 provider: option.value,
......@@ -5278,7 +5278,17 @@ const MessagesTimeline = memo(function MessagesTimeline({
52785278 );
52795279});
52805280
5281const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => option.available);
5281function isAvailableProviderOption(
5282 option: (typeof PROVIDER_OPTIONS)[number],
5283): option is {
5284 value: ProviderKind;
5285 label: string;
5286 available: true;
5287} {
5288 return option.available && option.value !== "claudeCode";
5289}
5290
5291const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter(isAvailableProviderOption);
52825292const UNAVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => !option.available);
52835293const COMING_SOON_PROVIDER_OPTIONS = [
52845294 { id: "opencode", label: "OpenCode", icon: OpenCodeIcon },
......@@ -5287,13 +5297,11 @@ const COMING_SOON_PROVIDER_OPTIONS = [
52875297
52885298function getCustomModelOptionsByProvider(settings: {
52895299 customCodexModels: readonly string[];
5290 customClaudeModels: readonly string[];
52915300 customCursorModels: readonly string[];
52925301}): Record<ProviderKind, ReadonlyArray<{ slug: string; name: string }>> {
52935302 const cursorFamilyOptions = getCursorModelFamilyOptions();
52945303 return {
52955304 codex: getAppModelOptions("codex", settings.customCodexModels),
5296 claudeCode: getAppModelOptions("claudeCode", settings.customClaudeModels),
52975305 cursor: [
52985306 ...cursorFamilyOptions,
52995307 ...getAppModelOptions("cursor", settings.customCursorModels).filter(
......@@ -5304,7 +5312,7 @@ function getCustomModelOptionsByProvider(settings: {
53045312 };
53055313}
53065314
5307const PROVIDER_ICON_BY_PROVIDER: Record<ProviderKind, Icon> = {
5315const PROVIDER_ICON_BY_PROVIDER: Record<ProviderPickerKind, Icon> = {
53085316 codex: OpenAI,
53095317 claudeCode: ClaudeAI,
53105318 cursor: CursorIcon,
......@@ -5385,10 +5393,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {
53855393 <span className="flex min-w-0 items-center gap-2">
53865394 <ProviderIcon
53875395 aria-hidden="true"
5388 className={cn(
5389 "size-4 shrink-0",
5390 props.provider === "claudeCode" ? "" : "text-muted-foreground/70",
5391 )}
5396 className="size-4 shrink-0 text-muted-foreground/70"
53925397 />
53935398 <span className="truncate">{selectedModelLabel}</span>
53945399 <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" />
apps/web/src/composerDraftStore.ts+1-1
......@@ -208,7 +208,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean {
208208}
209209
210210function normalizeProviderKind(value: unknown): ProviderKind | null {
211 return value === "codex" || value === "claudeCode" || value === "cursor" ? value : null;
211 return value === "codex" || value === "cursor" ? value : null;
212212}
213213
214214function revokeObjectPreviewUrl(previewUrl: string): void {
apps/web/src/routes/_chat.settings.tsx-7
......@@ -54,8 +54,6 @@ function getCustomModelsForProvider(
5454 provider: ProviderKind,
5555) {
5656 switch (provider) {
57 case "claudeCode":
58 return settings.customClaudeModels;
5957 case "cursor":
6058 return settings.customCursorModels;
6159 case "codex":
......@@ -69,8 +67,6 @@ function getDefaultCustomModelsForProvider(
6967 provider: ProviderKind,
7068) {
7169 switch (provider) {
72 case "claudeCode":
73 return defaults.customClaudeModels;
7470 case "cursor":
7571 return defaults.customCursorModels;
7672 case "codex":
......@@ -81,8 +77,6 @@ function getDefaultCustomModelsForProvider(
8177
8278function patchCustomModels(provider: ProviderKind, models: string[]) {
8379 switch (provider) {
84 case "claudeCode":
85 return { customClaudeModels: models };
8680 case "cursor":
8781 return { customCursorModels: models };
8882 case "codex":
......@@ -101,7 +95,6 @@ function SettingsRouteView() {
10195 Record<ProviderKind, string>
10296 >({
10397 codex: "",
104 claudeCode: "",
10598 cursor: "",
10699 });
107100 const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState<
apps/web/src/session-logic.ts+3-1
......@@ -10,8 +10,10 @@ import {
1010
1111import type { ChatMessage, ProposedPlan, SessionPhase, ThreadSession, TurnDiffSummary } from "./types";
1212
13export type ProviderPickerKind = ProviderKind | "claudeCode";
14
1315export const PROVIDER_OPTIONS: Array<{
14 value: ProviderKind;
16 value: ProviderPickerKind;
1517 label: string;
1618 available: boolean;
1719}> = [
apps/web/src/store.test.ts+2-24
......@@ -129,7 +129,7 @@ describe("store pure functions", () => {
129129});
130130
131131describe("store read model sync", () => {
132 it("preserves claude model slugs without an active session", () => {
132 it("falls back to the codex default for unsupported provider models without an active session", () => {
133133 const initialState = makeState(makeThread());
134134 const readModel = makeReadModel(
135135 makeReadModelThread({
......@@ -139,29 +139,7 @@ describe("store read model sync", () => {
139139
140140 const next = syncServerReadModel(initialState, readModel);
141141
142 expect(next.threads[0]?.model).toBe("claude-opus-4-6");
143 });
144
145 it("resolves claude aliases when session provider is claudeCode", () => {
146 const initialState = makeState(makeThread());
147 const readModel = makeReadModel(
148 makeReadModelThread({
149 model: "sonnet",
150 session: {
151 threadId: ThreadId.makeUnsafe("thread-1"),
152 status: "ready",
153 providerName: "claudeCode",
154 runtimeMode: "approval-required",
155 activeTurnId: null,
156 lastError: null,
157 updatedAt: "2026-02-27T00:00:00.000Z",
158 },
159 }),
160 );
161
162 const next = syncServerReadModel(initialState, readModel);
163
164 expect(next.threads[0]?.model).toBe("claude-sonnet-4-6");
142 expect(next.threads[0]?.model).toBe("gpt-5.3-codex");
165143 });
166144
167145 it("resolves cursor aliases when session provider is cursor", () => {
apps/web/src/store.ts+3-11
......@@ -145,19 +145,16 @@ function toLegacySessionStatus(
145145}
146146
147147function toLegacyProvider(providerName: string | null): ProviderKind {
148 if (providerName === "codex" || providerName === "claudeCode" || providerName === "cursor") {
148 if (providerName === "codex" || providerName === "cursor") {
149149 return providerName;
150150 }
151151 return "codex";
152152}
153153
154154const CODEX_MODEL_SLUGS = new Set<string>(getModelOptions("codex").map((option) => option.slug));
155const CLAUDE_MODEL_SLUGS = new Set<string>(getModelOptions("claudeCode").map((option) => option.slug));
156155const CURSOR_MODEL_SLUGS = new Set<string>(getModelOptions("cursor").map((option) => option.slug));
157156const CURSOR_DISTINCT_MODEL_SLUGS = new Set(
158 [...CURSOR_MODEL_SLUGS].filter(
159 (slug) => !CODEX_MODEL_SLUGS.has(slug) && !CLAUDE_MODEL_SLUGS.has(slug),
160 ),
157 [...CURSOR_MODEL_SLUGS].filter((slug) => !CODEX_MODEL_SLUGS.has(slug)),
161158);
162159
163160function inferProviderForThreadModel(input: {
......@@ -166,7 +163,6 @@ function inferProviderForThreadModel(input: {
166163}): ProviderKind {
167164 if (
168165 input.sessionProviderName === "codex" ||
169 input.sessionProviderName === "claudeCode" ||
170166 input.sessionProviderName === "cursor"
171167 ) {
172168 return input.sessionProviderName;
......@@ -175,10 +171,6 @@ function inferProviderForThreadModel(input: {
175171 if (normalizedCursor && CURSOR_DISTINCT_MODEL_SLUGS.has(normalizedCursor)) {
176172 return "cursor";
177173 }
178 const normalizedClaude = normalizeModelSlug(input.model, "claudeCode");
179 if (normalizedClaude && CLAUDE_MODEL_SLUGS.has(normalizedClaude)) {
180 return "claudeCode";
181 }
182174 const normalizedCodex = normalizeModelSlug(input.model, "codex");
183175 if (normalizedCodex && CODEX_MODEL_SLUGS.has(normalizedCodex)) {
184176 return "codex";
......@@ -190,7 +182,7 @@ function inferProviderForThreadModel(input: {
190182 ) {
191183 return "cursor";
192184 }
193 return input.model.trim().startsWith("claude-") ? "claudeCode" : "codex";
185 return "codex";
194186}
195187
196188function resolveWsHttpOrigin(): string {
packages/contracts/src/orchestration.test.ts+2-2
......@@ -128,11 +128,11 @@ it.effect("preserves explicit provider and runtime mode in thread.turn.start", (
128128 text: "hello",
129129 attachments: [],
130130 },
131 provider: "claudeCode",
131 provider: "cursor",
132132 runtimeMode: "full-access",
133133 createdAt: "2026-01-01T00:00:00.000Z",
134134 });
135 assert.strictEqual(parsed.provider, "claudeCode");
135 assert.strictEqual(parsed.provider, "cursor");
136136 assert.strictEqual(parsed.runtimeMode, "full-access");
137137 assert.strictEqual(parsed.interactionMode, DEFAULT_PROVIDER_INTERACTION_MODE);
138138 }),
packages/contracts/src/orchestration.ts+1-1
......@@ -27,7 +27,7 @@ export const ORCHESTRATION_WS_CHANNELS = {
2727 domainEvent: "orchestration.domainEvent",
2828} as const;
2929
30export const ProviderKind = Schema.Literals(["codex", "claudeCode", "cursor"]);
30export const ProviderKind = Schema.Literals(["codex", "cursor"]);
3131export type ProviderKind = typeof ProviderKind.Type;
3232export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex";
3333export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]);
packages/contracts/src/providerRuntime.test.ts+3-3
......@@ -10,7 +10,7 @@ describe("ProviderRuntimeEvent", () => {
1010 const parsed = decodeRuntimeEvent({
1111 type: "turn.plan.updated",
1212 eventId: "event-1",
13 provider: "claudeCode",
13 provider: "codex",
1414 sessionId: "runtime-session-1",
1515 createdAt: "2026-02-28T00:00:00.000Z",
1616 threadId: "thread-1",
......@@ -56,7 +56,7 @@ describe("ProviderRuntimeEvent", () => {
5656 const parsed = decodeRuntimeEvent({
5757 type: "user-input.requested",
5858 eventId: "event-2",
59 provider: "claudeCode",
59 provider: "codex",
6060 sessionId: "runtime-session-2",
6161 createdAt: "2026-02-28T00:00:01.000Z",
6262 threadId: "thread-2",
......@@ -94,7 +94,7 @@ describe("ProviderRuntimeEvent", () => {
9494 const parsed = decodeRuntimeEvent({
9595 type: "user-input.resolved",
9696 eventId: "event-3",
97 provider: "claudeCode",
97 provider: "codex",
9898 sessionId: "runtime-session-2",
9999 createdAt: "2026-02-28T00:00:02.000Z",
100100 threadId: "thread-2",
packages/contracts/src/providerRuntime.ts-2
......@@ -19,8 +19,6 @@ const RuntimeEventRawSource = Schema.Literals([
1919 "codex.app-server.notification",
2020 "codex.app-server.request",
2121 "codex.eventmsg",
22 "claude.sdk.message",
23 "claude.sdk.permission",
2422 "codex.sdk.thread-event",
2523 "cursor.acp.notification",
2624 "cursor.acp.request",
packages/shared/src/model.test.ts-1
......@@ -84,7 +84,6 @@ describe("resolveModelSlug", () => {
8484 it("keeps codex defaults for backward compatibility", () => {
8585 expect(getDefaultModel()).toBe(DEFAULT_MODEL);
8686 expect(getModelOptions()).toEqual(MODEL_OPTIONS);
87 expect(getModelOptions("claudeCode")).toEqual([]);
8887 expect(getModelOptions("cursor")).toEqual(MODEL_OPTIONS_BY_PROVIDER.cursor);
8988 expect(getCursorModelFamilyOptions()).toEqual(CURSOR_MODEL_FAMILY_OPTIONS);
9089 });