Renumbers the v2 migrations 044-052 to 045-053 behind main's 044_ClearAutomaticProjectModelDefaults, and adopts main's sticky new-thread selection (#9164), local-only worktree bases in the v2 launch path (#8751), the PR summary read for settlement (#9176), Claude per-cwd skills (#9210), the provider editor redesign with the branch's dedicated environment fields re-grafted (#8508), and the client half of continue-threads-across-restart (#9167). The server-side continuation markers stay unported: they live in the v1 session directory, and v2 recovery terminalizes running runs on restart, so the capability is withheld until the v2 runtime carries them.
Unified · Split
| @@ -153,6 +153,11 @@ export const COMPOSER_LAYOUT_TRANSITION = · 152 unmodified lines | ||
| 153 | 153 | ? undefined |
| 154 | 154 | : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System); |
| 155 | 155 | |
| 156 | +const COMPOSER_ATTACHMENT_ENTERING = | |
| 157 | + Platform.OS === "android" | |
| 158 | + ? FadeIn.duration(160) | |
| 159 | + : FadeIn.delay(COMPOSER_TRANSITION_DURATION_MS).duration(160).reduceMotion(ReduceMotion.System); | |
| 160 | + | |
| 156 | 161 | const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface); |
| 157 | 162 | |
| 158 | 163 | export function ComposerSurface(props: { |
| @@ -622,10 +627,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer · 463 unmodified lines | ||
| 622 | 627 | onPickFiles={props.onPickDraftFiles} |
| 623 | 628 | /> |
| 624 | 629 | ) : null} |
| 625 | − {isExpanded ? ( | |
| 630 | + {isExpanded && props.draftAttachments.length > 0 ? ( | |
| 626 | 631 | <Animated.View |
| 627 | − className={props.draftAttachments.length > 0 ? "px-[14px] pb-2.5" : undefined} | |
| 628 | − entering={FadeIn.duration(160)} | |
| 632 | + className="px-[14px] pb-2.5" | |
| 633 | + entering={COMPOSER_ATTACHMENT_ENTERING} | |
| 629 | 634 | exiting={FadeOut.duration(120)} |
| 630 | 635 | > |
| 631 | 636 | <ComposerAttachmentStrip |
| @@ -31,7 +31,6 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; · 30 unmodified lines | ||
| 31 | 31 | import * as RepositoryIdentityResolver from "../project/RepositoryIdentityResolver.ts"; |
| 32 | 32 | import * as ProjectService from "../project/ProjectService.ts"; |
| 33 | 33 | import * as T3ProjectFileLoader from "../project/T3ProjectFileLoader.ts"; |
| 34 | −import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts"; | |
| 35 | 34 | import { |
| 36 | 35 | clearPersistedServerRuntimeState, |
| 37 | 36 | readPersistedServerRuntimeState, |
| @@ -513,7 +512,6 @@ const projectAddCommand = Command.make("add", { · 475 unmodified lines | ||
| 513 | 512 | projectId, |
| 514 | 513 | title, |
| 515 | 514 | workspaceRoot, |
| 516 | − defaultModelSelection: ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), | |
| 517 | 515 | }); |
| 518 | 516 | return `Added project ${projectId} (${title}) at ${workspaceRoot}.`; |
| 519 | 517 | }), |
| @@ -245,7 +245,9 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { · 244 unmodified lines | ||
| 245 | 245 | expect(withFd.capabilities.serverSelfUpdate).toBe("desktop-managed"); |
| 246 | 246 | expect(withFd.capabilities.desktopAppUpdate).toBe(true); |
| 247 | 247 | expect(withFd.capabilities.serverSelfUpdateProgress).toBe(true); |
| 248 | − expect(withFd.capabilities.serverUpdateThreadContinuation).toBe(true); | |
| 248 | + // v2 recovery terminalizes running runs on restart, so continuation | |
| 249 | + // stays unadvertised until the v2 runtime carries the markers. | |
| 250 | + expect(withFd.capabilities.serverUpdateThreadContinuation).toBeUndefined(); | |
| 249 | 251 | |
| 250 | 252 | const withoutFd = yield* describeWith({ mode: "desktop" }); |
| 251 | 253 | expect(withoutFd.capabilities.serverSelfUpdate).toBe("desktop-managed"); |
| @@ -226,11 +226,12 @@ export const make = Effect.gen(function* () { · 225 unmodified lines | ||
| 226 | 226 | threadVisitedTracking: true, |
| 227 | 227 | threadPullRequestLinking: true, |
| 228 | 228 | ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }), |
| 229 | + // Running-thread continuation across a self-update (#9167) marks v1 | |
| 230 | + // provider session bindings; the v2 recovery path terminalizes running | |
| 231 | + // runs on restart instead, so the capability stays unadvertised until | |
| 232 | + // ProviderRuntimeRecoveryService grows an equivalent. | |
| 229 | 233 | ...(serverSelfUpdate === "boot-service" || desktopAppUpdate |
| 230 | − ? { | |
| 231 | − serverSelfUpdateProgress: true, | |
| 232 | − serverUpdateThreadContinuation: true, | |
| 233 | − } | |
| 234 | + ? { serverSelfUpdateProgress: true } | |
| 234 | 235 | : {}), |
| 235 | 236 | ...(desktopAppUpdate ? { desktopAppUpdate: true } : {}), |
| 236 | 237 | }, |
| @@ -240,20 +240,36 @@ export const make = Effect.gen(function* () { · 239 unmodified lines | ||
| 240 | 240 | .pipe(Effect.mapError(mapError(input, "update-thread", threadId))); |
| 241 | 241 | } |
| 242 | 242 | let startRef = input.workspaceStrategy.baseRef; |
| 243 | − if (input.workspaceStrategy.startFromOrigin === true) { | |
| 243 | + // "Start from origin" is a stored default; repos without the requested | |
| 244 | + // remote branch fall back to the local base branch. | |
| 245 | + const startFromOrigin = | |
| 246 | + input.workspaceStrategy.startFromOrigin === true && | |
| 247 | + (yield* git | |
| 248 | + .remoteExists({ cwd: project.workspaceRoot, remoteName: "origin" }) | |
| 249 | + .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))); | |
| 250 | + if (startFromOrigin) { | |
| 244 | 251 | yield* git |
| 245 | 252 | .fetchRemote({ cwd: project.workspaceRoot, remoteName: "origin" }) |
| 246 | 253 | .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId))); |
| 247 | − startRef = yield* git | |
| 248 | − .resolveRemoteTrackingCommit({ | |
| 254 | + const remoteBaseExists = yield* git | |
| 255 | + .remoteBranchExists({ | |
| 249 | 256 | cwd: project.workspaceRoot, |
| 250 | 257 | refName: input.workspaceStrategy.baseRef, |
| 251 | − fallbackRemoteName: "origin", | |
| 258 | + remoteName: "origin", | |
| 252 | 259 | }) |
| 253 | − .pipe( | |
| 254 | − Effect.map((resolved) => resolved.commitSha), | |
| 255 | − Effect.mapError(mapError(input, "provision-worktree", threadId)), | |
| 256 | − ); | |
| 260 | + .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId))); | |
| 261 | + if (remoteBaseExists) { | |
| 262 | + startRef = yield* git | |
| 263 | + .resolveRemoteTrackingCommit({ | |
| 264 | + cwd: project.workspaceRoot, | |
| 265 | + refName: input.workspaceStrategy.baseRef, | |
| 266 | + fallbackRemoteName: "origin", | |
| 267 | + }) | |
| 268 | + .pipe( | |
| 269 | + Effect.map((resolved) => resolved.commitSha), | |
| 270 | + Effect.mapError(mapError(input, "provision-worktree", threadId)), | |
| 271 | + ); | |
| 272 | + } | |
| 257 | 273 | } |
| 258 | 274 | const worktree = yield* git |
| 259 | 275 | .createWorktree({ |
| @@ -192,12 +192,18 @@ export const make = Effect.gen(function* () { · 191 unmodified lines | ||
| 192 | 192 | thread: (typeof candidates)[number], |
| 193 | 193 | ) { |
| 194 | 194 | if (thread.linkedPullRequest != null) { |
| 195 | − const detail = yield* pullRequests.detail({ | |
| 196 | − projectId: thread.linkedPullRequest.projectId, | |
| 197 | − repository: thread.linkedPullRequest.repository, | |
| 198 | − number: thread.linkedPullRequest.number, | |
| 199 | − }); | |
| 200 | − return { state: detail.state, updatedAt: detail.updatedAt } satisfies SettlementPullRequest; | |
| 195 | + const summary = yield* pullRequests.summary( | |
| 196 | + { | |
| 197 | + projectId: thread.linkedPullRequest.projectId, | |
| 198 | + repository: thread.linkedPullRequest.repository, | |
| 199 | + number: thread.linkedPullRequest.number, | |
| 200 | + }, | |
| 201 | + { recoverTransientFailure: false }, | |
| 202 | + ); | |
| 203 | + return { | |
| 204 | + state: summary.state, | |
| 205 | + updatedAt: summary.updatedAt, | |
| 206 | + } satisfies SettlementPullRequest; | |
| 201 | 207 | } |
| 202 | 208 | if (thread.branch === null) return null; |
| 203 | 209 | const workspaceRoot = workspaceRootByProjectId.get(thread.projectId); |
| @@ -219,7 +219,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" · 218 unmodified lines | ||
| 219 | 219 | projectId: command.projectId, |
| 220 | 220 | title: command.title, |
| 221 | 221 | workspaceRoot: command.workspaceRoot, |
| 222 | − defaultModelSelection: command.defaultModelSelection ?? null, | |
| 222 | + // Project creation has no user model choice. Older clients sent an | |
| 223 | + // automatic seed here, but only a metadata update records an | |
| 224 | + // explicit project default. | |
| 225 | + defaultModelSelection: null, | |
| 223 | 226 | faviconPath: null, |
| 224 | 227 | scripts: command.scripts ?? [], |
| 225 | 228 | createdAt: command.createdAt, |
| @@ -55,15 +55,16 @@ import Migration0040 from "./Migrations/040_ProjectionProjectFaviconPath.ts"; · 54 unmodified lines | ||
| 55 | 55 | import Migration0041 from "./Migrations/041_AuthSessionClientConnection.ts"; |
| 56 | 56 | import Migration0042 from "./Migrations/042_ProjectionThreadLinkedPullRequest.ts"; |
| 57 | 57 | import Migration0043 from "./Migrations/043_ProjectionThreadsUnsettledAt.ts"; |
| 58 | −import Migration0044 from "./Migrations/044_OrchestrationV2.ts"; | |
| 59 | −import Migration0045 from "./Migrations/045_OrchestrationV2Subagents.ts"; | |
| 60 | −import Migration0046 from "./Migrations/046_OrchestrationV2Foundation.ts"; | |
| 61 | −import Migration0047 from "./Migrations/047_OrchestrationV2ProviderSessionBindings.ts"; | |
| 62 | −import Migration0048 from "./Migrations/048_OrchestrationV2ThreadLaunchWorkflows.ts"; | |
| 63 | −import Migration0049 from "./Migrations/049_ApplicationEventSource.ts"; | |
| 64 | −import Migration0050 from "./Migrations/050_OrchestrationV2EffectCancellation.ts"; | |
| 65 | −import Migration0051 from "./Migrations/051_ScheduledTasks.ts"; | |
| 66 | −import Migration0052 from "./Migrations/052_LegacyV1ImportState.ts"; | |
| 58 | +import Migration0044 from "./Migrations/044_ClearAutomaticProjectModelDefaults.ts"; | |
| 59 | +import Migration0045 from "./Migrations/045_OrchestrationV2.ts"; | |
| 60 | +import Migration0046 from "./Migrations/046_OrchestrationV2Subagents.ts"; | |
| 61 | +import Migration0047 from "./Migrations/047_OrchestrationV2Foundation.ts"; | |
| 62 | +import Migration0048 from "./Migrations/048_OrchestrationV2ProviderSessionBindings.ts"; | |
| 63 | +import Migration0049 from "./Migrations/049_OrchestrationV2ThreadLaunchWorkflows.ts"; | |
| 64 | +import Migration0050 from "./Migrations/050_ApplicationEventSource.ts"; | |
| 65 | +import Migration0051 from "./Migrations/051_OrchestrationV2EffectCancellation.ts"; | |
| 66 | +import Migration0052 from "./Migrations/052_ScheduledTasks.ts"; | |
| 67 | +import Migration0053 from "./Migrations/053_LegacyV1ImportState.ts"; | |
| 67 | 68 | |
| 68 | 69 | /** |
| 69 | 70 | * Migration loader with all migrations defined inline. |
| @@ -119,15 +120,16 @@ export const migrationEntries = [ · 49 unmodified lines | ||
| 119 | 120 | [41, "AuthSessionClientConnection", Migration0041], |
| 120 | 121 | [42, "ProjectionThreadLinkedPullRequest", Migration0042], |
| 121 | 122 | [43, "ProjectionThreadsUnsettledAt", Migration0043], |
| 122 | − [44, "OrchestrationV2", Migration0044], | |
| 123 | − [45, "OrchestrationV2Subagents", Migration0045], | |
| 124 | − [46, "OrchestrationV2Foundation", Migration0046], | |
| 125 | − [47, "OrchestrationV2ProviderSessionBindings", Migration0047], | |
| 126 | − [48, "OrchestrationV2ThreadLaunchWorkflows", Migration0048], | |
| 127 | − [49, "ApplicationEventSource", Migration0049], | |
| 128 | − [50, "OrchestrationV2EffectCancellation", Migration0050], | |
| 129 | − [51, "ScheduledTasks", Migration0051], | |
| 130 | − [52, "LegacyV1ImportState", Migration0052], | |
| 123 | + [44, "ClearAutomaticProjectModelDefaults", Migration0044], | |
| 124 | + [45, "OrchestrationV2", Migration0045], | |
| 125 | + [46, "OrchestrationV2Subagents", Migration0046], | |
| 126 | + [47, "OrchestrationV2Foundation", Migration0047], | |
| 127 | + [48, "OrchestrationV2ProviderSessionBindings", Migration0048], | |
| 128 | + [49, "OrchestrationV2ThreadLaunchWorkflows", Migration0049], | |
| 129 | + [50, "ApplicationEventSource", Migration0050], | |
| 130 | + [51, "OrchestrationV2EffectCancellation", Migration0051], | |
| 131 | + [52, "ScheduledTasks", Migration0052], | |
| 132 | + [53, "LegacyV1ImportState", Migration0053], | |
| 131 | 133 | ] as const; |
| 132 | 134 | |
| 133 | 135 | export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); |
| @@ -8,12 +8,12 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; · 7 unmodified lines | ||
| 8 | 8 | |
| 9 | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 | |
| 11 | −layer("038_039_OrchestrationV2", (it) => { | |
| 11 | +layer("045_046_OrchestrationV2", (it) => { | |
| 12 | 12 | it.effect("keeps released and private migration ids contiguous", () => |
| 13 | 13 | Effect.sync(() => { |
| 14 | 14 | assert.deepStrictEqual( |
| 15 | 15 | migrationEntries.map(([id]) => id), |
| 16 | − Array.from({ length: 52 }, (_, index) => index + 1), | |
| 16 | + Array.from({ length: 53 }, (_, index) => index + 1), | |
| 17 | 17 | ); |
| 18 | 18 | }), |
| 19 | 19 | ); |
| @@ -22,7 +22,7 @@ layer("038_039_OrchestrationV2", (it) => { · 2 unmodified lines | ||
| 22 | 22 | Effect.gen(function* () { |
| 23 | 23 | const sql = yield* SqlClient.SqlClient; |
| 24 | 24 | |
| 25 | − yield* runMigrations({ toMigrationInclusive: 45 }); | |
| 25 | + yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 26 | 26 | |
| 27 | 27 | const migrations = yield* sql<{ |
| 28 | 28 | readonly migration_id: number; |
| @@ -30,24 +30,24 @@ layer("038_039_OrchestrationV2", (it) => { · 1 unmodified line | ||
| 30 | 30 | }>` |
| 31 | 31 | SELECT migration_id, name |
| 32 | 32 | FROM effect_sql_migrations |
| 33 | − WHERE migration_id IN (42, 43, 44, 45) | |
| 33 | + WHERE migration_id IN (43, 44, 45, 46) | |
| 34 | 34 | ORDER BY migration_id |
| 35 | 35 | `; |
| 36 | 36 | assert.deepStrictEqual(migrations, [ |
| 37 | − { | |
| 38 | − migration_id: 42, | |
| 39 | − name: "ProjectionThreadLinkedPullRequest", | |
| 40 | − }, | |
| 41 | 37 | { |
| 42 | 38 | migration_id: 43, |
| 43 | 39 | name: "ProjectionThreadsUnsettledAt", |
| 44 | 40 | }, |
| 45 | 41 | { |
| 46 | 42 | migration_id: 44, |
| 47 | − name: "OrchestrationV2", | |
| 43 | + name: "ClearAutomaticProjectModelDefaults", | |
| 48 | 44 | }, |
| 49 | 45 | { |
| 50 | 46 | migration_id: 45, |
| 47 | + name: "OrchestrationV2", | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + migration_id: 46, | |
| 51 | 51 | name: "OrchestrationV2Subagents", |
| 52 | 52 | }, |
| 53 | 53 | ]); |
| @@ -64,10 +64,10 @@ layer("038_039_OrchestrationV2", (it) => { · 10 unmodified lines | ||
| 64 | 64 | }), |
| 65 | 65 | ); |
| 66 | 66 | |
| 67 | − it.effect("backfills provider-session thread bindings in migration 047", () => | |
| 67 | + it.effect("backfills provider-session thread bindings in migration 048", () => | |
| 68 | 68 | Effect.gen(function* () { |
| 69 | 69 | const sql = yield* SqlClient.SqlClient; |
| 70 | − yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 70 | + yield* runMigrations({ toMigrationInclusive: 47 }); | |
| 71 | 71 | yield* sql` |
| 72 | 72 | INSERT INTO orchestration_v2_projection_provider_sessions ( |
| 73 | 73 | provider_session_id, |
| @@ -92,7 +92,7 @@ layer("038_039_OrchestrationV2", (it) => { · 18 unmodified lines | ||
| 92 | 92 | ) |
| 93 | 93 | `; |
| 94 | 94 | |
| 95 | − yield* runMigrations({ toMigrationInclusive: 47 }); | |
| 95 | + yield* runMigrations({ toMigrationInclusive: 48 }); | |
| 96 | 96 | |
| 97 | 97 | const bindings = yield* sql<{ |
| 98 | 98 | readonly provider_session_id: string; |
| @@ -110,10 +110,10 @@ layer("038_039_OrchestrationV2", (it) => { · 11 unmodified lines | ||
| 110 | 110 | }), |
| 111 | 111 | ); |
| 112 | 112 | |
| 113 | − it.effect("preserves turn items with colliding ordinals in migration 046", () => | |
| 113 | + it.effect("preserves turn items with colliding ordinals in migration 047", () => | |
| 114 | 114 | Effect.gen(function* () { |
| 115 | 115 | const sql = yield* SqlClient.SqlClient; |
| 116 | − yield* runMigrations({ toMigrationInclusive: 45 }); | |
| 116 | + yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 117 | 117 | yield* sql` |
| 118 | 118 | INSERT INTO orchestration_v2_projection_runs ( |
| 119 | 119 | run_id, |
| @@ -150,7 +150,7 @@ layer("038_039_OrchestrationV2", (it) => { · 30 unmodified lines | ||
| 150 | 150 | ('turn-item:d', 'thread:two', NULL, 42, 'assistant_message', 'completed', '2026-01-01T00:00:00.000Z', '{}') |
| 151 | 151 | `; |
| 152 | 152 | |
| 153 | − yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 153 | + yield* runMigrations({ toMigrationInclusive: 47 }); | |
| 154 | 154 | |
| 155 | 155 | const positions = yield* sql<{ |
| 156 | 156 | readonly thread_id: string; |
| @@ -182,7 +182,7 @@ it.effect("upgrades a database already at released main migration 043", () => · 25 unmodified lines | ||
| 182 | 182 | assert.ok(snoozeColumns.some((column) => column.name === "snoozed_until")); |
| 183 | 183 | assert.ok(snoozeColumns.some((column) => column.name === "snoozed_at")); |
| 184 | 184 | |
| 185 | − yield* runMigrations({ toMigrationInclusive: 52 }); | |
| 185 | + yield* runMigrations({ toMigrationInclusive: 53 }); | |
| 186 | 186 | |
| 187 | 187 | const migrations = yield* sql<{ |
| 188 | 188 | readonly migration_id: number; |
| @@ -190,7 +190,7 @@ it.effect("upgrades a database already at released main migration 043", () => · 1 unmodified line | ||
| 190 | 190 | }>` |
| 191 | 191 | SELECT migration_id, name |
| 192 | 192 | FROM effect_sql_migrations |
| 193 | − WHERE migration_id BETWEEN 39 AND 52 | |
| 193 | + WHERE migration_id BETWEEN 39 AND 53 | |
| 194 | 194 | ORDER BY migration_id |
| 195 | 195 | `; |
| 196 | 196 | assert.deepStrictEqual( |
| @@ -201,15 +201,16 @@ it.effect("upgrades a database already at released main migration 043", () => · 4 unmodified lines | ||
| 201 | 201 | [41, "AuthSessionClientConnection"], |
| 202 | 202 | [42, "ProjectionThreadLinkedPullRequest"], |
| 203 | 203 | [43, "ProjectionThreadsUnsettledAt"], |
| 204 | − [44, "OrchestrationV2"], | |
| 205 | − [45, "OrchestrationV2Subagents"], | |
| 206 | − [46, "OrchestrationV2Foundation"], | |
| 207 | − [47, "OrchestrationV2ProviderSessionBindings"], | |
| 208 | − [48, "OrchestrationV2ThreadLaunchWorkflows"], | |
| 209 | − [49, "ApplicationEventSource"], | |
| 210 | − [50, "OrchestrationV2EffectCancellation"], | |
| 211 | − [51, "ScheduledTasks"], | |
| 212 | − [52, "LegacyV1ImportState"], | |
| 204 | + [44, "ClearAutomaticProjectModelDefaults"], | |
| 205 | + [45, "OrchestrationV2"], | |
| 206 | + [46, "OrchestrationV2Subagents"], | |
| 207 | + [47, "OrchestrationV2Foundation"], | |
| 208 | + [48, "OrchestrationV2ProviderSessionBindings"], | |
| 209 | + [49, "OrchestrationV2ThreadLaunchWorkflows"], | |
| 210 | + [50, "ApplicationEventSource"], | |
| 211 | + [51, "OrchestrationV2EffectCancellation"], | |
| 212 | + [52, "ScheduledTasks"], | |
| 213 | + [53, "LegacyV1ImportState"], | |
| 213 | 214 | ], |
| 214 | 215 | ); |
| 215 | 216 | |
File renamed without changes.
File renamed without changes.
| @@ -8,7 +8,7 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; · 7 unmodified lines | ||
| 8 | 8 | it.effect("backfills the V2 event driver from payload before the legacy provider column", () => |
| 9 | 9 | Effect.gen(function* () { |
| 10 | 10 | const sql = yield* SqlClient.SqlClient; |
| 11 | − yield* runMigrations({ toMigrationInclusive: 45 }); | |
| 11 | + yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 12 | 12 | |
| 13 | 13 | yield* sql` |
| 14 | 14 | INSERT INTO orchestration_v2_events ( |
| @@ -37,7 +37,7 @@ it.effect("backfills the V2 event driver from payload before the legacy provider · 22 unmodified lines | ||
| 37 | 37 | ) |
| 38 | 38 | `; |
| 39 | 39 | |
| 40 | − yield* runMigrations({ toMigrationInclusive: 46 }); | |
| 40 | + yield* runMigrations({ toMigrationInclusive: 47 }); | |
| 41 | 41 | |
| 42 | 42 | const events = yield* sql<{ |
| 43 | 43 | readonly event_id: string; |
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -8,11 +8,11 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; · 7 unmodified lines | ||
| 8 | 8 | |
| 9 | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 | |
| 11 | −layer("043_ApplicationEventSource", (it) => { | |
| 11 | +layer("050_ApplicationEventSource", (it) => { | |
| 12 | 12 | it.effect("moves V2 events and current project state behind one global sequence", () => |
| 13 | 13 | Effect.gen(function* () { |
| 14 | 14 | const sql = yield* SqlClient.SqlClient; |
| 15 | − yield* runMigrations({ toMigrationInclusive: 48 }); | |
| 15 | + yield* runMigrations({ toMigrationInclusive: 49 }); | |
| 16 | 16 | |
| 17 | 17 | yield* sql` |
| 18 | 18 | INSERT INTO orchestration_v2_events ( |
| @@ -84,7 +84,7 @@ layer("043_ApplicationEventSource", (it) => { · 65 unmodified lines | ||
| 84 | 84 | ) |
| 85 | 85 | `; |
| 86 | 86 | |
| 87 | − yield* runMigrations({ toMigrationInclusive: 49 }); | |
| 87 | + yield* runMigrations({ toMigrationInclusive: 50 }); | |
| 88 | 88 | |
| 89 | 89 | const events = yield* sql<{ |
| 90 | 90 | readonly sequence: number; |
File renamed without changes.
| @@ -8,11 +8,11 @@ import * as NodeSqliteClient from "../NodeSqliteClient.ts"; · 7 unmodified lines | ||
| 8 | 8 | |
| 9 | 9 | const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); |
| 10 | 10 | |
| 11 | −layer("050_OrchestrationV2EffectCancellation", (it) => { | |
| 11 | +layer("051_OrchestrationV2EffectCancellation", (it) => { | |
| 12 | 12 | it.effect("preserves existing effects and adds the cancelled terminal status", () => |
| 13 | 13 | Effect.gen(function* () { |
| 14 | 14 | const sql = yield* SqlClient.SqlClient; |
| 15 | − yield* runMigrations({ toMigrationInclusive: 49 }); | |
| 15 | + yield* runMigrations({ toMigrationInclusive: 50 }); | |
| 16 | 16 | yield* sql` |
| 17 | 17 | INSERT INTO orchestration_v2_effect_outbox ( |
| 18 | 18 | effect_id, |
| @@ -47,7 +47,7 @@ layer("050_OrchestrationV2EffectCancellation", (it) => { · 28 unmodified lines | ||
| 47 | 47 | ) |
| 48 | 48 | `; |
| 49 | 49 | |
| 50 | − yield* runMigrations({ toMigrationInclusive: 50 }); | |
| 50 | + yield* runMigrations({ toMigrationInclusive: 51 }); | |
| 51 | 51 | yield* sql` |
| 52 | 52 | UPDATE orchestration_v2_effect_outbox |
| 53 | 53 | SET |
File renamed without changes.
File renamed without changes.
File renamed without changes.
| @@ -61,6 +61,7 @@ import { · 60 unmodified lines | ||
| 61 | 61 | type ProviderSnapshotSettings, |
| 62 | 62 | } from "../providerUpdateSettings.ts"; |
| 63 | 63 | import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts"; |
| 64 | +import { discoverClaudeSkills } from "./ClaudeSkills.ts"; | |
| 64 | 65 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); |
| 65 | 66 | |
| 66 | 67 | const DRIVER_KIND = ProviderDriverKind.make("claudeAgent"); |
| @@ -246,6 +247,17 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { · 179 unmodified lines | ||
| 246 | 247 | accentColor, |
| 247 | 248 | enabled, |
| 248 | 249 | snapshot, |
| 250 | + snapshotForCwd: (cwd: string) => | |
| 251 | + !effectiveConfig.enabled | |
| 252 | + ? snapshot.getSnapshot | |
| 253 | + : Effect.all([ | |
| 254 | + snapshot.getSnapshot, | |
| 255 | + discoverClaudeSkills(effectiveConfig, cwd, processEnv), | |
| 256 | + ]).pipe( | |
| 257 | + Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills })), | |
| 258 | + Effect.provideService(FileSystem.FileSystem, fileSystem), | |
| 259 | + Effect.provideService(Path.Path, path), | |
| 260 | + ), | |
| 249 | 261 | orchestrationAdapter, |
| 250 | 262 | textGeneration, |
| 251 | 263 | } satisfies ProviderInstance; |
| @@ -10,7 +10,7 @@ import * as ServerConfig from "./config.ts"; · 9 unmodified lines | ||
| 10 | 10 | import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; |
| 11 | 11 | |
| 12 | 12 | it("uses the canonical Codex model for auto-bootstrap", () => { |
| 13 | − assert.deepEqual(ServerRuntimeStartup.getAutoBootstrapDefaultModelSelection(), { | |
| 13 | + assert.deepEqual(ServerRuntimeStartup.getAutoBootstrapThreadModelSelection(), { | |
| 14 | 14 | instanceId: ProviderInstanceId.make("codex"), |
| 15 | 15 | model: DEFAULT_MODEL, |
| 16 | 16 | }); |
| @@ -176,7 +176,7 @@ export const launchStartupHeartbeat = recordStartupHeartbeat.pipe( · 175 unmodified lines | ||
| 176 | 176 | Effect.asVoid, |
| 177 | 177 | ); |
| 178 | 178 | |
| 179 | −export const getAutoBootstrapDefaultModelSelection = (): ModelSelection => ({ | |
| 179 | +export const getAutoBootstrapThreadModelSelection = (): ModelSelection => ({ | |
| 180 | 180 | instanceId: ProviderInstanceId.make("codex"), |
| 181 | 181 | model: DEFAULT_MODEL, |
| 182 | 182 | }); |
| @@ -210,13 +210,14 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { · 27 unmodified lines | ||
| 210 | 210 | let bootstrapThreadId: ThreadId | undefined; |
| 211 | 211 | |
| 212 | 212 | if (serverConfig.autoBootstrapProjectFromCwd) { |
| 213 | − const defaultModelSelection = getAutoBootstrapDefaultModelSelection(); | |
| 213 | + // Project creation has no user model choice; only the bootstrap thread | |
| 214 | + // gets an automatic selection, and an explicit project default wins. | |
| 215 | + const threadModelSelection = getAutoBootstrapThreadModelSelection(); | |
| 214 | 216 | const { project } = yield* projects.bootstrap({ |
| 215 | 217 | commandId: CommandId.make(yield* randomUUID), |
| 216 | 218 | projectId: ProjectId.make(yield* randomUUID), |
| 217 | 219 | title: path.basename(serverConfig.cwd) || "project", |
| 218 | 220 | workspaceRoot: serverConfig.cwd, |
| 219 | − defaultModelSelection, | |
| 220 | 221 | }); |
| 221 | 222 | const shell = yield* threads.getShellSnapshot(); |
| 222 | 223 | const existingThread = shell.threads.find( |
| @@ -228,7 +229,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { · 5 unmodified lines | ||
| 228 | 229 | commandId: CommandId.make(yield* randomUUID), |
| 229 | 230 | projectId: project.id, |
| 230 | 231 | title: "New thread", |
| 231 | − modelSelection: project.defaultModelSelection ?? defaultModelSelection, | |
| 232 | + modelSelection: project.defaultModelSelection ?? threadModelSelection, | |
| 232 | 233 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 233 | 234 | runtimeMode: "full-access", |
| 234 | 235 | workspaceStrategy: { type: "root" }, |
| @@ -176,7 +176,6 @@ import * as SourceControlProviderRegistry from "./sourceControl/SourceControlPro · 175 unmodified lines | ||
| 176 | 176 | import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; |
| 177 | 177 | import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; |
| 178 | 178 | import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts"; |
| 179 | −import * as VcsProcess from "./vcs/VcsProcess.ts"; | |
| 180 | 179 | import * as PairingGrantStore from "./auth/PairingGrantStore.ts"; |
| 181 | 180 | import * as SessionStore from "./auth/SessionStore.ts"; |
| 182 | 181 | import { failEnvironmentAuthInvalid, failEnvironmentInternal } from "./auth/http.ts"; |
| @@ -1840,6 +1839,10 @@ const makeWsRpcLayer = ( · 1657 unmodified lines | ||
| 1840 | 1839 | observeRpcEffect(WS_METHODS.pullRequestsListStats, pullRequests.listStats(input), { |
| 1841 | 1840 | "rpc.aggregate": "pull-requests", |
| 1842 | 1841 | }), |
| 1842 | + [WS_METHODS.pullRequestsSummary]: (input) => | |
| 1843 | + observeRpcEffect(WS_METHODS.pullRequestsSummary, pullRequests.summary(input), { | |
| 1844 | + "rpc.aggregate": "pull-requests", | |
| 1845 | + }), | |
| 1843 | 1846 | [WS_METHODS.pullRequestsDetail]: (input) => |
| 1844 | 1847 | observeRpcEffect(WS_METHODS.pullRequestsDetail, pullRequests.detail(input), { |
| 1845 | 1848 | "rpc.aggregate": "pull-requests", |
| @@ -2612,7 +2615,6 @@ export const websocketRpcRouteLayer = Layer.unwrap( · 766 unmodified lines | ||
| 2612 | 2615 | ), |
| 2613 | 2616 | ), |
| 2614 | 2617 | ), |
| 2615 | − Layer.provide(VcsProcess.layer), | |
| 2616 | 2618 | ), |
| 2617 | 2619 | ), |
| 2618 | 2620 | ), |
| @@ -406,6 +406,7 @@ import { · 405 unmodified lines | ||
| 406 | 406 | resolveServerSelfUpdateCapability, |
| 407 | 407 | serverUpdateGuidance, |
| 408 | 408 | supportsDesktopAppUpdate, |
| 409 | + supportsServerUpdateThreadContinuation, | |
| 409 | 410 | } from "../versionSkew"; |
| 410 | 411 | import { useAssetUrls } from "../assets/assetUrls"; |
| 411 | 412 | import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates"; |
| @@ -2394,6 +2395,7 @@ function ChatViewContent(props: ChatViewProps) { · 1982 unmodified lines | ||
| 2394 | 2395 | const serverUpdateEnvironmentId = activeThread?.environmentId ?? null; |
| 2395 | 2396 | const versionMismatchSelfUpdate = resolveServerSelfUpdateCapability(serverConfig); |
| 2396 | 2397 | const versionMismatchDesktopAppUpdate = supportsDesktopAppUpdate(serverConfig); |
| 2398 | + const versionMismatchThreadContinuation = supportsServerUpdateThreadContinuation(serverConfig); | |
| 2397 | 2399 | const serverUpdateState = useAtomValue( |
| 2398 | 2400 | serverEnvironment.updateStateAtom(serverUpdateEnvironmentId), |
| 2399 | 2401 | ); |
| @@ -2527,6 +2529,7 @@ function ChatViewContent(props: ChatViewProps) { · 127 unmodified lines | ||
| 2527 | 2529 | serverLabel={versionMismatchServerLabel} |
| 2528 | 2530 | selfUpdate={versionMismatchSelfUpdate} |
| 2529 | 2531 | desktopAppUpdate={versionMismatchDesktopAppUpdate} |
| 2532 | + threadContinuation={versionMismatchThreadContinuation} | |
| 2530 | 2533 | targetVersion={versionMismatch.clientVersion} |
| 2531 | 2534 | label={updateFailed ? "Retry" : "Update"} |
| 2532 | 2535 | variant="ghost" |
| @@ -2561,6 +2564,7 @@ function ChatViewContent(props: ChatViewProps) { · 28 unmodified lines | ||
| 2561 | 2564 | serverUpdateEnvironmentId, |
| 2562 | 2565 | versionMismatchSelfUpdate, |
| 2563 | 2566 | versionMismatchDesktopAppUpdate, |
| 2567 | + versionMismatchThreadContinuation, | |
| 2564 | 2568 | versionMismatchServerLabel, |
| 2565 | 2569 | ]); |
| 2566 | 2570 | const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS; |
| @@ -4429,7 +4429,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) · 4428 unmodified lines | ||
| 4429 | 4429 | ) : null} |
| 4430 | 4430 | <ComposerFooterPrimaryActions |
| 4431 | 4431 | compact={isComposerPrimaryActionsCompact} |
| 4432 | − activeContextWindow={activeContextWindow} | |
| 4432 | + activeContextWindow={ | |
| 4433 | + settings.contextWindowMeterEnabled ? activeContextWindow : null | |
| 4434 | + } | |
| 4433 | 4435 | activeThreadModelDisplayName={activeThreadModelDisplayName} |
| 4434 | 4436 | pendingAction={pendingPrimaryAction} |
| 4435 | 4437 | isRunning={phase === "running"} |
| @@ -5,6 +5,8 @@ import { · 4 unmodified lines | ||
| 5 | 5 | CopyIcon, |
| 6 | 6 | DownloadIcon, |
| 7 | 7 | LoaderIcon, |
| 8 | + LockIcon, | |
| 9 | + LockOpenIcon, | |
| 8 | 10 | PlusIcon, |
| 9 | 11 | Trash2Icon, |
| 10 | 12 | XIcon, |
| @@ -33,12 +35,10 @@ import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; · 22 unmodified lines | ||
| 33 | 35 | import { normalizeProviderAccentColor } from "../../providerInstances"; |
| 34 | 36 | import { Badge } from "../ui/badge"; |
| 35 | 37 | import { Button } from "../ui/button"; |
| 36 | −import { Checkbox } from "../ui/checkbox"; | |
| 37 | 38 | import { DraftInput } from "../ui/draft-input"; |
| 38 | 39 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 39 | 40 | import { ScrollArea } from "../ui/scroll-area"; |
| 40 | 41 | import { Switch } from "../ui/switch"; |
| 41 | −import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; | |
| 42 | 42 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 43 | 43 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 44 | 44 | import type { DriverOption, ProviderEnvironmentFieldDefinition } from "./providerDriverMeta"; |
| @@ -58,6 +58,13 @@ import { · 13 unmodified lines | ||
| 58 | 58 | |
| 59 | 59 | const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; |
| 60 | 60 | |
| 61 | +/** Label-left field grid for the Configuration tab: one row per field. */ | |
| 62 | +const PROVIDER_FIELD_GRID_CLASS_NAME = | |
| 63 | + "grid gap-x-4 gap-y-2.5 sm:grid-cols-[8rem_minmax(0,1fr)] sm:items-start"; | |
| 64 | +/** Full-width divider row that names the group of fields below it. */ | |
| 65 | +const PROVIDER_FIELD_GROUP_LABEL_CLASS_NAME = | |
| 66 | + "col-span-full mt-1 border-t border-border/60 pt-2.5 text-[11px] text-muted-foreground"; | |
| 67 | + | |
| 61 | 68 | let environmentVariableDraftId = 0; |
| 62 | 69 | const nextEnvironmentVariableDraftId = () => `provider-env-${environmentVariableDraftId++}`; |
| 63 | 70 | |
| @@ -130,6 +137,43 @@ function nextConfigBlobWithValue( · 66 unmodified lines | ||
| 130 | 137 | return base; |
| 131 | 138 | } |
| 132 | 139 | |
| 140 | +export function deriveProviderModelsForDisplay(input: { | |
| 141 | + readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined; | |
| 142 | + readonly customModels: ReadonlyArray<string>; | |
| 143 | +}): ReadonlyArray<ServerProviderModel> { | |
| 144 | + const liveCustomModelsBySlug = new Map( | |
| 145 | + Arr.filterMap(input.liveModels ?? [], (model) => | |
| 146 | + model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid, | |
| 147 | + ), | |
| 148 | + ); | |
| 149 | + const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? []; | |
| 150 | + const customModels = input.customModels.map( | |
| 151 | + (slug) => | |
| 152 | + liveCustomModelsBySlug.get(slug) ?? { | |
| 153 | + slug, | |
| 154 | + name: slug, | |
| 155 | + isCustom: true, | |
| 156 | + capabilities: null, | |
| 157 | + }, | |
| 158 | + ); | |
| 159 | + return [...serverModels, ...customModels]; | |
| 160 | +} | |
| 161 | + | |
| 162 | +function ProviderAuthEmail(props: { readonly email: string | undefined }) { | |
| 163 | + const email = props.email?.trim(); | |
| 164 | + if (!email) return null; | |
| 165 | + | |
| 166 | + return ( | |
| 167 | + <RedactedSensitiveText | |
| 168 | + value={email} | |
| 169 | + ariaLabel="Toggle account email visibility" | |
| 170 | + revealTooltip="Click to reveal email" | |
| 171 | + hideTooltip="Click to hide email" | |
| 172 | + className="max-w-full truncate" | |
| 173 | + /> | |
| 174 | + ); | |
| 175 | +} | |
| 176 | + | |
| 133 | 177 | export function readProviderEnvironmentVariable( |
| 134 | 178 | environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined, |
| 135 | 179 | name: string, |
| @@ -179,38 +223,50 @@ export function nextProviderEnvironmentWithFieldValue( · 43 unmodified lines | ||
| 179 | 223 | return next; |
| 180 | 224 | } |
| 181 | 225 | |
| 182 | −export function deriveProviderModelsForDisplay(input: { | |
| 183 | − readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined; | |
| 184 | − readonly customModels: ReadonlyArray<CustomModelDefinition>; | |
| 185 | −}): ReadonlyArray<ServerProviderModel> { | |
| 186 | − const liveCustomModelsBySlug = new Map( | |
| 187 | − Arr.filterMap(input.liveModels ?? [], (model) => | |
| 188 | − model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid, | |
| 189 | − ), | |
| 190 | − ); | |
| 191 | − const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? []; | |
| 192 | − const customModels = input.customModels.map((entry) => ({ | |
| 193 | − slug: entry.slug, | |
| 194 | − name: entry.name, | |
| 195 | − isCustom: true, | |
| 196 | − capabilities: | |
| 197 | − entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null, | |
| 198 | − })); | |
| 199 | − return [...serverModels, ...customModels]; | |
| 200 | −} | |
| 201 | − | |
| 202 | −function ProviderAuthEmail(props: { readonly email: string | undefined }) { | |
| 203 | − const email = props.email?.trim(); | |
| 204 | − if (!email) return null; | |
| 226 | +function ProviderEnvironmentFieldRow(props: { | |
| 227 | + readonly field: ProviderEnvironmentFieldDefinition; | |
| 228 | + readonly variable: ProviderInstanceEnvironmentVariable | undefined; | |
| 229 | + readonly idPrefix: string; | |
| 230 | + readonly onCommit: (field: ProviderEnvironmentFieldDefinition, value: string) => void; | |
| 231 | + readonly onRemove: (field: ProviderEnvironmentFieldDefinition) => void; | |
| 232 | +}) { | |
| 233 | + const inputId = `${props.idPrefix}-environment-${props.field.name}`; | |
| 234 | + const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? ""); | |
| 235 | + const placeholder = props.variable?.valueRedacted | |
| 236 | + ? "Stored secret - enter a new value to replace" | |
| 237 | + : props.field.placeholder; | |
| 205 | 238 | |
| 206 | 239 | return ( |
| 207 | − <RedactedSensitiveText | |
| 208 | − value={email} | |
| 209 | − ariaLabel="Toggle account email visibility" | |
| 210 | − revealTooltip="Click to reveal email" | |
| 211 | − hideTooltip="Click to hide email" | |
| 212 | − className="max-w-full truncate" | |
| 213 | − /> | |
| 240 | + <label htmlFor={inputId} className="block"> | |
| 241 | + <span className="text-xs font-medium text-foreground">{props.field.label}</span> | |
| 242 | + <div className="mt-1.5 flex min-w-0 items-center gap-2"> | |
| 243 | + <DraftInput | |
| 244 | + id={inputId} | |
| 245 | + className="min-w-0 flex-1" | |
| 246 | + type={props.field.sensitive === false ? undefined : "password"} | |
| 247 | + autoComplete="off" | |
| 248 | + value={value} | |
| 249 | + onCommit={(next) => props.onCommit(props.field, next)} | |
| 250 | + placeholder={placeholder} | |
| 251 | + spellCheck={false} | |
| 252 | + /> | |
| 253 | + {props.variable ? ( | |
| 254 | + <Button | |
| 255 | + type="button" | |
| 256 | + size="icon-sm" | |
| 257 | + variant="ghost" | |
| 258 | + className="size-8 shrink-0 text-muted-foreground hover:text-destructive" | |
| 259 | + onClick={() => props.onRemove(props.field)} | |
| 260 | + aria-label={`Clear ${props.field.label}`} | |
| 261 | + > | |
| 262 | + <XIcon className="size-3.5" /> | |
| 263 | + </Button> | |
| 264 | + ) : null} | |
| 265 | + </div> | |
| 266 | + {props.field.description ? ( | |
| 267 | + <span className="mt-1 block text-xs text-muted-foreground">{props.field.description}</span> | |
| 268 | + ) : null} | |
| 269 | + </label> | |
| 214 | 270 | ); |
| 215 | 271 | } |
| 216 | 272 | |
| @@ -284,166 +340,105 @@ function ProviderEnvironmentSection(props: { · 67 unmodified lines | ||
| 284 | 340 | publishRows(nextRows); |
| 285 | 341 | }; |
| 286 | 342 | |
| 287 | − return ( | |
| 288 | − <div className="grid gap-2"> | |
| 289 | − <div className="flex items-center justify-between gap-3"> | |
| 290 | − <span className="text-xs font-medium text-foreground">Environment variables</span> | |
| 291 | − <Button | |
| 292 | − type="button" | |
| 293 | − size="sm" | |
| 294 | − variant="outline" | |
| 295 | − className="h-7 gap-1.5 px-2 text-xs" | |
| 296 | − onClick={() => | |
| 297 | − setRows([ | |
| 298 | − ...rows, | |
| 299 | − { | |
| 300 | − id: nextEnvironmentVariableDraftId(), | |
| 301 | − name: "", | |
| 302 | − value: "", | |
| 303 | − sensitive: true, | |
| 304 | − }, | |
| 305 | − ]) | |
| 306 | − } | |
| 307 | − > | |
| 308 | − <PlusIcon className="size-3" /> | |
| 309 | − Add | |
| 310 | − </Button> | |
| 311 | − </div> | |
| 312 | − {rows.length === 0 ? ( | |
| 313 | − <p className="text-xs text-muted-foreground"> | |
| 314 | − Add variables to pass API keys, base URLs, or other per-instance CLI settings. | |
| 315 | − </p> | |
| 316 | − ) : ( | |
| 317 | − <div className="overflow-hidden rounded-md border border-border/70"> | |
| 318 | − <Table> | |
| 319 | − <TableHeader className="bg-muted/25 text-[11px] text-muted-foreground"> | |
| 320 | − <TableRow className="hover:bg-transparent"> | |
| 321 | − <TableHead>Variable</TableHead> | |
| 322 | − <TableHead>Value</TableHead> | |
| 323 | − <TableHead className="w-20">Sensitive</TableHead> | |
| 324 | − <TableHead className="w-12 text-right"> | |
| 325 | − <span className="sr-only">Options</span> | |
| 326 | − </TableHead> | |
| 327 | − </TableRow> | |
| 328 | − </TableHeader> | |
| 329 | − <TableBody> | |
| 330 | − {rows.map((variable, index) => ( | |
| 331 | − <TableRow | |
| 332 | − key={variable.id} | |
| 333 | − className="border-border/60 odd:bg-muted/20 even:bg-background/20" | |
| 334 | − > | |
| 335 | − <TableCell> | |
| 336 | − <DraftInput | |
| 337 | − value={variable.name} | |
| 338 | − onCommit={(name) => updateVariable(variable.id, { name: name.trim() })} | |
| 339 | − placeholder="VARIABLE_NAME" | |
| 340 | − spellCheck={false} | |
| 341 | − aria-label={`Environment variable name ${index + 1}`} | |
| 342 | − /> | |
| 343 | − </TableCell> | |
| 344 | − <TableCell> | |
| 345 | − <DraftInput | |
| 346 | − value={variable.valueRedacted ? "" : variable.value} | |
| 347 | − onCommit={(value) => updateVariable(variable.id, { value })} | |
| 348 | − type={variable.sensitive ? "password" : undefined} | |
| 349 | − autoComplete="off" | |
| 350 | − placeholder={ | |
| 351 | − variable.valueRedacted | |
| 352 | − ? "Stored secret - enter a new value to replace" | |
| 353 | − : "Value" | |
| 354 | − } | |
| 355 | − spellCheck={false} | |
| 356 | − aria-label={`Environment variable value ${index + 1}`} | |
| 357 | − /> | |
| 358 | − </TableCell> | |
| 359 | − <TableCell className="w-20"> | |
| 360 | − <div className="flex h-8 items-center justify-center"> | |
| 361 | − <Checkbox | |
| 362 | − checked={variable.sensitive} | |
| 363 | − onCheckedChange={(checked) => { | |
| 364 | − const sensitive = Boolean(checked); | |
| 365 | − updateVariable(variable.id, { | |
| 366 | − sensitive, | |
| 367 | − ...(sensitive && variable.valueRedacted === undefined | |
| 368 | − ? {} | |
| 369 | − : { valueRedacted: sensitive ? variable.valueRedacted : false }), | |
| 370 | − }); | |
| 371 | − }} | |
| 372 | − aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`} | |
| 373 | − /> | |
| 374 | − </div> | |
| 375 | − </TableCell> | |
| 376 | − <TableCell className="w-12"> | |
| 377 | − <div className="flex justify-end"> | |
| 378 | − <Button | |
| 379 | − type="button" | |
| 380 | − size="icon-sm" | |
| 381 | − variant="ghost" | |
| 382 | − className="size-8 text-muted-foreground hover:text-destructive" | |
| 383 | − onClick={() => removeVariable(variable.id)} | |
| 384 | − aria-label={`Remove environment variable ${variable.name || index + 1}`} | |
| 385 | − > | |
| 386 | − <XIcon className="size-3.5" /> | |
| 387 | − </Button> | |
| 388 | − </div> | |
| 389 | − </TableCell> | |
| 390 | − </TableRow> | |
| 391 | − ))} | |
| 392 | − </TableBody> | |
| 393 | − </Table> | |
| 394 | − </div> | |
| 395 | − )} | |
| 396 | − <span className="text-xs text-muted-foreground"> | |
| 397 | − Sensitive values are stored separately and are not returned to the app after saving. | |
| 398 | − </span> | |
| 399 | − </div> | |
| 400 | − ); | |
| 401 | −} | |
| 402 | − | |
| 403 | −function ProviderEnvironmentFieldRow(props: { | |
| 404 | − readonly field: ProviderEnvironmentFieldDefinition; | |
| 405 | − readonly variable: ProviderInstanceEnvironmentVariable | undefined; | |
| 406 | − readonly idPrefix: string; | |
| 407 | − readonly onCommit: (field: ProviderEnvironmentFieldDefinition, value: string) => void; | |
| 408 | − readonly onRemove: (field: ProviderEnvironmentFieldDefinition) => void; | |
| 409 | −}) { | |
| 410 | − const inputId = `${props.idPrefix}-environment-${props.field.name}`; | |
| 411 | − const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? ""); | |
| 412 | − const placeholder = props.variable?.valueRedacted | |
| 413 | − ? "Stored secret - enter a new value to replace" | |
| 414 | − : props.field.placeholder; | |
| 343 | + const addVariable = () => | |
| 344 | + setRows([ | |
| 345 | + ...rows, | |
| 346 | + { | |
| 347 | + id: nextEnvironmentVariableDraftId(), | |
| 348 | + name: "", | |
| 349 | + value: "", | |
| 350 | + sensitive: true, | |
| 351 | + }, | |
| 352 | + ]); | |
| 415 | 353 | |
| 416 | 354 | return ( |
| 417 | − <label htmlFor={inputId} className="block"> | |
| 418 | − <span className="text-xs font-medium text-foreground">{props.field.label}</span> | |
| 419 | − <div className="mt-1.5 flex min-w-0 items-center gap-2"> | |
| 420 | − <DraftInput | |
| 421 | − id={inputId} | |
| 422 | − className="min-w-0 flex-1" | |
| 423 | − type={props.field.sensitive === false ? undefined : "password"} | |
| 424 | − autoComplete="off" | |
| 425 | − value={value} | |
| 426 | − onCommit={(next) => props.onCommit(props.field, next)} | |
| 427 | − placeholder={placeholder} | |
| 428 | − spellCheck={false} | |
| 429 | − /> | |
| 430 | − {props.variable ? ( | |
| 355 | + <div className="min-w-0 space-y-1.5"> | |
| 356 | + {rows.map((variable, index) => ( | |
| 357 | + <div key={variable.id} className="flex min-w-0 items-center gap-1.5"> | |
| 358 | + <DraftInput | |
| 359 | + size="compact" | |
| 360 | + className="w-44 shrink-0 font-mono" | |
| 361 | + value={variable.name} | |
| 362 | + onCommit={(name) => updateVariable(variable.id, { name: name.trim() })} | |
| 363 | + placeholder="VARIABLE_NAME" | |
| 364 | + spellCheck={false} | |
| 365 | + aria-label={`Environment variable name ${index + 1}`} | |
| 366 | + /> | |
| 367 | + <span className="text-xs text-muted-foreground" aria-hidden> | |
| 368 | + = | |
| 369 | + </span> | |
| 370 | + <DraftInput | |
| 371 | + size="compact" | |
| 372 | + className="min-w-0 flex-1 font-mono" | |
| 373 | + value={variable.valueRedacted ? "" : variable.value} | |
| 374 | + onCommit={(value) => updateVariable(variable.id, { value })} | |
| 375 | + type={variable.sensitive ? "password" : undefined} | |
| 376 | + autoComplete="off" | |
| 377 | + placeholder={ | |
| 378 | + variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value" | |
| 379 | + } | |
| 380 | + spellCheck={false} | |
| 381 | + aria-label={`Environment variable value ${index + 1}`} | |
| 382 | + /> | |
| 383 | + <Tooltip> | |
| 384 | + <TooltipTrigger | |
| 385 | + render={ | |
| 386 | + <Button | |
| 387 | + type="button" | |
| 388 | + size="icon-micro" | |
| 389 | + variant="ghost-muted" | |
| 390 | + className={cn( | |
| 391 | + "[--control-icon-color:currentColor]", | |
| 392 | + variable.sensitive && "text-foreground", | |
| 393 | + )} | |
| 394 | + onClick={() => { | |
| 395 | + const sensitive = !variable.sensitive; | |
| 396 | + updateVariable(variable.id, { | |
| 397 | + sensitive, | |
| 398 | + ...(sensitive && variable.valueRedacted === undefined | |
| 399 | + ? {} | |
| 400 | + : { valueRedacted: sensitive ? variable.valueRedacted : false }), | |
| 401 | + }); | |
| 402 | + }} | |
| 403 | + aria-pressed={variable.sensitive} | |
| 404 | + aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`} | |
| 405 | + > | |
| 406 | + {variable.sensitive ? ( | |
| 407 | + <LockIcon className="size-3" /> | |
| 408 | + ) : ( | |
| 409 | + <LockOpenIcon className="size-3" /> | |
| 410 | + )} | |
| 411 | + </Button> | |
| 412 | + } | |
| 413 | + /> | |
| 414 | + <TooltipPopup side="top"> | |
| 415 | + {variable.sensitive ? "Sensitive, stored separately" : "Plain text"} | |
| 416 | + </TooltipPopup> | |
| 417 | + </Tooltip> | |
| 431 | 418 | <Button |
| 432 | 419 | type="button" |
| 433 | − size="icon-sm" | |
| 434 | − variant="ghost" | |
| 435 | − className="size-8 shrink-0 text-muted-foreground hover:text-destructive" | |
| 436 | − onClick={() => props.onRemove(props.field)} | |
| 437 | − aria-label={`Clear ${props.field.label}`} | |
| 420 | + size="icon-micro" | |
| 421 | + variant="ghost-muted" | |
| 422 | + className="[--control-icon-color:currentColor] hover:text-destructive" | |
| 423 | + onClick={() => removeVariable(variable.id)} | |
| 424 | + aria-label={`Remove environment variable ${variable.name || index + 1}`} | |
| 438 | 425 | > |
| 439 | − <XIcon className="size-3.5" /> | |
| 426 | + <XIcon className="size-3" /> | |
| 440 | 427 | </Button> |
| 441 | − ) : null} | |
| 428 | + </div> | |
| 429 | + ))} | |
| 430 | + <div className="flex min-h-[1.875rem] flex-wrap items-center gap-x-3 gap-y-1"> | |
| 431 | + <Button type="button" size="xs" variant="ghost-muted" onClick={addVariable}> | |
| 432 | + <PlusIcon className="size-3" /> | |
| 433 | + Add variable | |
| 434 | + </Button> | |
| 435 | + <span className="text-xs text-muted-foreground"> | |
| 436 | + {rows.length === 0 | |
| 437 | + ? "API keys, base URLs, or other per-instance CLI settings." | |
| 438 | + : "Sensitive values are stored separately and never returned to the app."} | |
| 439 | + </span> | |
| 442 | 440 | </div> |
| 443 | − {props.field.description ? ( | |
| 444 | − <span className="mt-1 block text-xs text-muted-foreground">{props.field.description}</span> | |
| 445 | − ) : null} | |
| 446 | − </label> | |
| 441 | + </div> | |
| 447 | 442 | ); |
| 448 | 443 | } |
| 449 | 444 | |
| @@ -458,7 +453,7 @@ interface ProviderInstanceCardProps { · 8 unmodified lines | ||
| 458 | 453 | readonly readOnly?: boolean | undefined; |
| 459 | 454 | readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void; |
| 460 | 455 | /** |
| 461 | − * Pass `undefined` to hide the delete button entirely. Built-in default | |
| 456 | + * Pass `undefined` to hide the delete footer entirely. Built-in default | |
| 462 | 457 | * instance slots use `undefined` — they can't be deleted without losing |
| 463 | 458 | * the slot, and their "reset to defaults" affordance lives on an outer |
| 464 | 459 | * reset button instead. Explicit `| undefined` in the type accommodates |
| @@ -578,12 +573,6 @@ export function ProviderInstanceCard({ · 113 unmodified lines | ||
| 578 | 573 | const visibleTab = driverOption === undefined ? "configuration" : activeTab; |
| 579 | 574 | |
| 580 | 575 | const customModels = readConfigStringArray(instance.config, "customModels"); |
| 581 | − const environmentFields = driverOption?.environmentFields ?? []; | |
| 582 | − const environmentFieldNames = new Set(environmentFields.map((field) => field.name)); | |
| 583 | − const genericEnvironment = providerEnvironmentWithoutNames( | |
| 584 | − instance.environment, | |
| 585 | − environmentFieldNames, | |
| 586 | − ); | |
| 587 | 576 | // Server-returned models may lag behind settings writes. Treat probe |
| 588 | 577 | // models as the source for built-ins only; custom rows come directly |
| 589 | 578 | // from the current instance config so add/remove reflects immediately. |
| @@ -591,6 +580,9 @@ export function ProviderInstanceCard({ · 1 unmodified line | ||
| 591 | 580 | liveModels: liveProvider?.models, |
| 592 | 581 | customModels, |
| 593 | 582 | }); |
| 583 | + const hiddenModelCount = modelsForDisplay.filter( | |
| 584 | + (model) => !model.isCustom && hiddenModels.includes(model.slug), | |
| 585 | + ).length; | |
| 594 | 586 | |
| 595 | 587 | const updateDisplayName = (value: string) => { |
| 596 | 588 | const trimmed = value.trim(); |
| @@ -644,7 +636,14 @@ export function ProviderInstanceCard({ · 47 unmodified lines | ||
| 644 | 636 | : (rest as ProviderInstanceConfig), |
| 645 | 637 | ); |
| 646 | 638 | }; |
| 647 | − | |
| 639 | + // Drivers that need a named secret (Cursor's API key) get a dedicated field; | |
| 640 | + // the generic editor only shows the remaining variables. | |
| 641 | + const environmentFields = driverOption?.environmentFields ?? []; | |
| 642 | + const environmentFieldNames = new Set(environmentFields.map((field) => field.name)); | |
| 643 | + const genericEnvironment = providerEnvironmentWithoutNames( | |
| 644 | + instance.environment, | |
| 645 | + environmentFieldNames, | |
| 646 | + ); | |
| 648 | 647 | const updateGenericEnvironment = ( |
| 649 | 648 | environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>, |
| 650 | 649 | ) => { |
| @@ -653,11 +652,9 @@ export function ProviderInstanceCard({ · 2 unmodified lines | ||
| 653 | 652 | ); |
| 654 | 653 | updateEnvironment([...dedicatedEnvironment, ...environment]); |
| 655 | 654 | }; |
| 656 | − | |
| 657 | 655 | const updateEnvironmentField = (field: ProviderEnvironmentFieldDefinition, value: string) => { |
| 658 | 656 | updateEnvironment(nextProviderEnvironmentWithFieldValue(instance.environment, field, value)); |
| 659 | 657 | }; |
| 660 | − | |
| 661 | 658 | const removeEnvironmentField = (field: ProviderEnvironmentFieldDefinition) => { |
| 662 | 659 | updateEnvironment(providerEnvironmentWithoutNames(instance.environment, new Set([field.name]))); |
| 663 | 660 | }; |
| @@ -704,35 +701,9 @@ export function ProviderInstanceCard({ · 40 unmodified lines | ||
| 704 | 701 | </> |
| 705 | 702 | ); |
| 706 | 703 | |
| 707 | − const titleTailNode = ( | |
| 708 | − <> | |
| 709 | − {headerAction ? ( | |
| 710 | − <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center"> | |
| 711 | − {headerAction} | |
| 712 | − </span> | |
| 713 | − ) : null} | |
| 714 | − {onDelete ? ( | |
| 715 | − <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center"> | |
| 716 | − <Tooltip> | |
| 717 | − <TooltipTrigger | |
| 718 | − render={ | |
| 719 | − <Button | |
| 720 | − size="icon-micro" | |
| 721 | − variant="ghost" | |
| 722 | − className="text-muted-foreground hover:text-destructive" | |
| 723 | − onClick={onDelete} | |
| 724 | − aria-label={`Delete provider instance ${instanceId}`} | |
| 725 | − > | |
| 726 | − <Trash2Icon className="size-3" /> | |
| 727 | − </Button> | |
| 728 | − } | |
| 729 | − /> | |
| 730 | − <TooltipPopup side="top">Delete instance</TooltipPopup> | |
| 731 | − </Tooltip> | |
| 732 | − </span> | |
| 733 | − ) : null} | |
| 734 | − </> | |
| 735 | − ); | |
| 704 | + const titleTailNode = headerAction ? ( | |
| 705 | + <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">{headerAction}</span> | |
| 706 | + ) : null; | |
| 736 | 707 | |
| 737 | 708 | const versionCodeNode = versionLabel ? ( |
| 738 | 709 | <code className="text-xs text-muted-foreground">{versionLabel}</code> |
| @@ -757,7 +728,7 @@ export function ProviderInstanceCard({ · 18 unmodified lines | ||
| 757 | 728 | className={cn( |
| 758 | 729 | // Sidebar-style selection with a fixed row height so the list stays |
| 759 | 730 | // even; the status line clamps to two lines instead of growing. |
| 760 | − "group flex h-19 items-start gap-3 rounded-md px-3 py-2 transition-colors", | |
| 731 | + "group flex min-h-19 items-start gap-3 rounded-md px-3 py-2 transition-colors", | |
| 761 | 732 | // Foreground-alpha tint so the fill reads the same in light and dark themes. |
| 762 | 733 | selected ? "bg-foreground/8" : "hover:bg-foreground/4", |
| 763 | 734 | )} |
| @@ -839,7 +810,7 @@ export function ProviderInstanceCard({ · 75 unmodified lines | ||
| 839 | 810 | size="icon-xs" |
| 840 | 811 | variant="ghost" |
| 841 | 812 | className={cn( |
| 842 | − "size-5 rounded-sm p-0", | |
| 813 | + "size-5 rounded-sm p-0 [--control-icon-color:currentColor]", | |
| 843 | 814 | versionAdvisory.emphasis === "strong" |
| 844 | 815 | ? "text-warning hover:text-warning" |
| 845 | 816 | : "text-muted-foreground hover:text-foreground", |
| @@ -947,6 +918,25 @@ export function ProviderInstanceCard({ · 101 unmodified lines | ||
| 947 | 918 | </p> |
| 948 | 919 | ) : null} |
| 949 | 920 | </div> |
| 921 | + {onDelete ? ( | |
| 922 | + <span | |
| 923 | + inert={readOnly} | |
| 924 | + aria-disabled={readOnly || undefined} | |
| 925 | + className={cn("shrink-0", readOnly && "opacity-50")} | |
| 926 | + > | |
| 927 | + <Button | |
| 928 | + type="button" | |
| 929 | + size="xs" | |
| 930 | + variant="ghost" | |
| 931 | + className="[--control-icon-color:currentColor] text-muted-foreground hover:text-destructive" | |
| 932 | + onClick={onDelete} | |
| 933 | + aria-label={`Delete instance ${instanceId}`} | |
| 934 | + > | |
| 935 | + <Trash2Icon className="size-3" /> | |
| 936 | + Delete instance | |
| 937 | + </Button> | |
| 938 | + </span> | |
| 939 | + ) : null} | |
| 950 | 940 | </div> |
| 951 | 941 | |
| 952 | 942 | <div className="flex h-11 shrink-0 border-b border-border/70 px-1"> |
| @@ -962,10 +952,14 @@ export function ProviderInstanceCard({ · 9 unmodified lines | ||
| 962 | 952 | <button |
| 963 | 953 | type="button" |
| 964 | 954 | aria-pressed={visibleTab === "models"} |
| 965 | − className={providerSettingsTabClassName(visibleTab === "models")} | |
| 955 | + className={cn(providerSettingsTabClassName(visibleTab === "models"), "gap-1.5")} | |
| 966 | 956 | onClick={() => setActiveTab("models")} |
| 967 | 957 | > |
| 968 | 958 | Models |
| 959 | + <span className="font-normal text-muted-foreground/70"> | |
| 960 | + {modelsForDisplay.length} | |
| 961 | + {hiddenModelCount > 0 ? ` · ${hiddenModelCount} hidden` : ""} | |
| 962 | + </span> | |
| 969 | 963 | </button> |
| 970 | 964 | ) : null} |
| 971 | 965 | </div> |
| @@ -980,76 +974,74 @@ export function ProviderInstanceCard({ · 8 unmodified lines | ||
| 980 | 974 | <div |
| 981 | 975 | inert={readOnly} |
| 982 | 976 | aria-disabled={readOnly || undefined} |
| 983 | − className={cn("space-y-5 px-4 py-5", readOnly && "opacity-50 select-none")} | |
| 977 | + className={cn("px-4 py-4", readOnly && "opacity-50 select-none")} | |
| 984 | 978 | > |
| 985 | − <div> | |
| 986 | − <label htmlFor={`provider-instance-${instanceId}-display-name`} className="block"> | |
| 987 | − <span className="text-xs font-medium text-foreground">Display name</span> | |
| 979 | + <div className={PROVIDER_FIELD_GRID_CLASS_NAME}> | |
| 980 | + <label | |
| 981 | + htmlFor={`provider-instance-${instanceId}-display-name`} | |
| 982 | + className="pt-1.5 text-xs font-medium text-foreground" | |
| 983 | + > | |
| 984 | + Display name | |
| 985 | + </label> | |
| 986 | + <div className="flex min-w-0 flex-wrap items-center gap-3"> | |
| 988 | 987 | <DraftInput |
| 989 | 988 | id={`provider-instance-${instanceId}-display-name`} |
| 990 | − className="mt-1.5" | |
| 989 | + className="w-44" | |
| 991 | 990 | value={instance.displayName ?? ""} |
| 992 | 991 | onCommit={updateDisplayName} |
| 993 | 992 | placeholder={driverOption?.label ?? "Instance label"} |
| 994 | 993 | spellCheck={false} |
| 995 | 994 | /> |
| 996 | − <span className="mt-1 block text-xs text-muted-foreground"> | |
| 997 | − Optional label shown in the provider list. | |
| 998 | − </span> | |
| 999 | − </label> | |
| 1000 | − </div> | |
| 1001 | − | |
| 1002 | − <div> | |
| 1003 | − <ProviderAccentColorPicker | |
| 1004 | − displayName={displayName} | |
| 1005 | − value={accentColor} | |
| 1006 | − onCommit={updateAccentColor} | |
| 1007 | − commitDelayMs={120} | |
| 1008 | − description="Used to distinguish this instance in picker rails and model lists." | |
| 1009 | − /> | |
| 1010 | − </div> | |
| 1011 | − | |
| 1012 | − <div> | |
| 1013 | − {environmentFields.length > 0 ? ( | |
| 1014 | − <div className="mb-4 grid gap-3"> | |
| 1015 | − {environmentFields.map((field) => ( | |
| 1016 | − <ProviderEnvironmentFieldRow | |
| 1017 | − key={field.name} | |
| 1018 | − field={field} | |
| 1019 | − variable={readProviderEnvironmentVariable(instance.environment, field.name)} | |
| 1020 | − idPrefix={`provider-instance-${instanceId}`} | |
| 1021 | − onCommit={updateEnvironmentField} | |
| 1022 | − onRemove={removeEnvironmentField} | |
| 1023 | − /> | |
| 1024 | − ))} | |
| 1025 | − </div> | |
| 1026 | − ) : null} | |
| 1027 | − <ProviderEnvironmentSection | |
| 1028 | − environment={genericEnvironment} | |
| 1029 | − onChange={updateGenericEnvironment} | |
| 1030 | − /> | |
| 1031 | − </div> | |
| 1032 | − | |
| 1033 | − {driverOption ? ( | |
| 1034 | − <ProviderSettingsForm | |
| 1035 | − definition={driverOption} | |
| 1036 | − value={instance.config} | |
| 1037 | − idPrefix={`provider-instance-${instanceId}`} | |
| 1038 | − variant="card" | |
| 1039 | − onChange={updateConfig} | |
| 1040 | − /> | |
| 1041 | − ) : null} | |
| 1042 | − | |
| 1043 | − {driverOption === undefined ? ( | |
| 1044 | − <div> | |
| 1045 | − <p className="text-xs text-muted-foreground"> | |
| 1046 | − This instance uses a driver ( | |
| 1047 | − <code className="text-foreground">{String(instance.driver)}</code>) that is not | |
| 1048 | − shipped with the current build. Configuration values are preserved but cannot be | |
| 1049 | − edited from this surface. | |
| 1050 | − </p> | |
| 995 | + <ProviderAccentColorPicker | |
| 996 | + layout="inline" | |
| 997 | + displayName={displayName} | |
| 998 | + value={accentColor} | |
| 999 | + onCommit={updateAccentColor} | |
| 1000 | + commitDelayMs={120} | |
| 1001 | + /> | |
| 1051 | 1002 | </div> |
| 1052 | − ) : null} | |
| 1003 | + <div className={PROVIDER_FIELD_GROUP_LABEL_CLASS_NAME}>Runtime</div> | |
| 1004 | + {driverOption ? ( | |
| 1005 | + <ProviderSettingsForm | |
| 1006 | + definition={driverOption} | |
| 1007 | + value={instance.config} | |
| 1008 | + idPrefix={`provider-instance-${instanceId}`} | |
| 1009 | + variant="grid" | |
| 1010 | + onChange={updateConfig} | |
| 1011 | + /> | |
| 1012 | + ) : ( | |
| 1013 | + <> | |
| 1014 | + <span className="pt-1.5 text-xs font-medium text-foreground">Driver</span> | |
| 1015 | + <p className="text-xs text-muted-foreground"> | |
| 1016 | + This instance uses a driver ( | |
| 1017 | + <code className="text-foreground">{String(instance.driver)}</code>) that is not | |
| 1018 | + shipped with the current build. Configuration values are preserved but cannot be | |
| 1019 | + edited from this surface. | |
| 1020 | + </p> | |
| 1021 | + </> | |
| 1022 | + )} | |
| 1023 | + <div className={PROVIDER_FIELD_GROUP_LABEL_CLASS_NAME}>Environment</div> | |
| 1024 | + <div className="col-span-full"> | |
| 1025 | + {environmentFields.length > 0 ? ( | |
| 1026 | + <div className="mb-4 grid gap-3"> | |
| 1027 | + {environmentFields.map((field) => ( | |
| 1028 | + <ProviderEnvironmentFieldRow | |
| 1029 | + key={field.name} | |
| 1030 | + field={field} | |
| 1031 | + variable={readProviderEnvironmentVariable(instance.environment, field.name)} | |
| 1032 | + idPrefix={`provider-instance-${instanceId}`} | |
| 1033 | + onCommit={updateEnvironmentField} | |
| 1034 | + onRemove={removeEnvironmentField} | |
| 1035 | + /> | |
| 1036 | + ))} | |
| 1037 | + </div> | |
| 1038 | + ) : null} | |
| 1039 | + <ProviderEnvironmentSection | |
| 1040 | + environment={genericEnvironment} | |
| 1041 | + onChange={updateGenericEnvironment} | |
| 1042 | + /> | |
| 1043 | + </div> | |
| 1044 | + </div> | |
| 1053 | 1045 | </div> |
| 1054 | 1046 | </ScrollArea> |
| 1055 | 1047 | {driverOption !== undefined ? ( |
| @@ -1821,8 +1821,8 @@ code { · 1820 unmodified lines | ||
| 1821 | 1821 | } |
| 1822 | 1822 | |
| 1823 | 1823 | .chat-markdown ul { |
| 1824 | − /* Reset for nested uls under a widened ol — --list-gutter is an inherited | |
| 1825 | − custom property, so without this a task-list under a 3+ digit ordered | |
| 1824 | + /* Reset for nested uls under a widened ol. --list-gutter is an inherited | |
| 1825 | + custom property, so without this a task-list under a multi-digit ordered | |
| 1826 | 1826 | list would inherit the outer gutter instead of its own default. */ |
| 1827 | 1827 | --list-gutter: 1.25rem; |
| 1828 | 1828 | padding-left: 1.25rem; |
| @@ -1831,7 +1831,7 @@ code { · 2 unmodified lines | ||
| 1831 | 1831 | |
| 1832 | 1832 | /* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but |
| 1833 | 1833 | ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose |
| 1834 | − widest marker is 3+ characters, so item 100+ isn't clipped by list-style-position: | |
| 1834 | + widest marker has multiple characters so it isn't clipped by list-style-position: | |
| 1835 | 1835 | outside painting the marker past the padding box. Reset it here too so a |
| 1836 | 1836 | nested ol without its own widened marker doesn't inherit the outer one. */ |
| 1837 | 1837 | .chat-markdown ol { |
| @@ -208,6 +208,9 @@ export const ClientSettingsSchema = Schema.Struct({ · 207 unmodified lines | ||
| 208 | 208 | confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 209 | 209 | confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 210 | 210 | confirmThreadUnpin: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 211 | + continueThreadsAfterServerUpdate: Schema.Boolean.pipe( | |
| 212 | + Schema.withDecodingDefault(Effect.succeed(false)), | |
| 213 | + ), | |
| 211 | 214 | dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe( |
| 212 | 215 | Schema.withDecodingDefault(Effect.succeed([])), |
| 213 | 216 | ), |
| @@ -269,6 +272,9 @@ export const ClientSettingsSchema = Schema.Struct({ · 55 unmodified lines | ||
| 269 | 272 | // default UI; this beta flag restores it (plus the /plan and /default slash |
| 270 | 273 | // commands) for users who still rely on the old workflow. |
| 271 | 274 | planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 275 | + // Legacy context window meter. The composer hides it by default; users who | |
| 276 | + // still want the old usage indicator can restore it from Settings. | |
| 277 | + contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | |
| 272 | 278 | showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 273 | 279 | // Legacy sidebar (the original per-project tree). Deliberately a fresh key |
| 274 | 280 | // (was `sidebarV2Enabled` + `sidebarV2ConfiguredByUser`): decoding drops the |
| @@ -1009,6 +1015,7 @@ export const ClientSettingsPatch = Schema.Struct({ · 734 unmodified lines | ||
| 1009 | 1015 | confirmThreadArchive: Schema.optionalKey(Schema.Boolean), |
| 1010 | 1016 | confirmThreadDelete: Schema.optionalKey(Schema.Boolean), |
| 1011 | 1017 | confirmThreadUnpin: Schema.optionalKey(Schema.Boolean), |
| 1018 | + continueThreadsAfterServerUpdate: Schema.optionalKey(Schema.Boolean), | |
| 1012 | 1019 | diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), |
| 1013 | 1020 | environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), |
| 1014 | 1021 | glassOpacity: Schema.optionalKey(GlassOpacity), |
| @@ -1044,6 +1051,7 @@ export const ClientSettingsPatch = Schema.Struct({ · 29 unmodified lines | ||
| 1044 | 1051 | ), |
| 1045 | 1052 | ), |
| 1046 | 1053 | planModeEnabled: Schema.optionalKey(Schema.Boolean), |
| 1054 | + contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), | |
| 1047 | 1055 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), |
| 1048 | 1056 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), |
| 1049 | 1057 | sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), |
| @@ -3,6 +3,7 @@ import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from " · 2 unmodified lines | ||
| 3 | 3 | |
| 4 | 4 | import { |
| 5 | 5 | applyClaudePromptEffortPrefix, |
| 6 | + buildExplicitProviderOptionSelectionsFromDescriptors, | |
| 6 | 7 | buildProviderOptionSelectionsFromDescriptors, |
| 7 | 8 | createModelCapabilities, |
| 8 | 9 | createModelSelection, |
| @@ -127,6 +128,22 @@ describe("descriptor helpers", () => { · 118 unmodified lines | ||
| 127 | 128 | ]); |
| 128 | 129 | }); |
| 129 | 130 | |
| 131 | + it("builds dispatch options only from explicit selections", () => { | |
| 132 | + const descriptors = getProviderOptionDescriptors({ | |
| 133 | + caps: codexCaps, | |
| 134 | + selections: [{ id: "fastMode", value: true }], | |
| 135 | + }); | |
| 136 | + | |
| 137 | + expect(buildExplicitProviderOptionSelectionsFromDescriptors(descriptors, undefined)).toBe( | |
| 138 | + undefined, | |
| 139 | + ); | |
| 140 | + expect( | |
| 141 | + buildExplicitProviderOptionSelectionsFromDescriptors(descriptors, [ | |
| 142 | + { id: "fastMode", value: true }, | |
| 143 | + ]), | |
| 144 | + ).toEqual([{ id: "fastMode", value: true }]); | |
| 145 | + }); | |
| 146 | + | |
| 130 | 147 | it("stores option selection arrays in model selections", () => { |
| 131 | 148 | expect( |
| 132 | 149 | createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.4", [ |
| @@ -472,7 +472,7 @@ importers: · 471 unmodified lines | ||
| 472 | 472 | dependencies: |
| 473 | 473 | '@anthropic-ai/claude-agent-sdk': |
| 474 | 474 | specifier: ^0.3.205 |
| 475 | − version: 0.3.252(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) | |
| 475 | + version: 0.3.257(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) | |
| 476 | 476 | '@connectrpc/connect': |
| 477 | 477 | specifier: 1.7.0 |
| 478 | 478 | version: 1.7.0(@bufbuild/protobuf@1.10.0) |
| @@ -1012,8 +1012,8 @@ packages: · 533 unmodified lines | ||
| 1012 | 1012 | '@alchemy.run/node-utils@0.0.5': |
| 1013 | 1013 | resolution: {integrity: sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ==} |
| 1014 | 1014 | |
| 1015 | − '@anthropic-ai/claude-agent-sdk@0.3.252': | |
| 1016 | − resolution: {integrity: sha512-hCkyZFn3J46aAMNqS6AZbYz91FaLUmX5VvJOzYZqzlVBJN47OxXQugqOzqa6b6GOZRmwiqW2ck8J8TE7bQZswQ==} | |
| 1015 | + '@anthropic-ai/claude-agent-sdk@0.3.257': | |
| 1016 | + resolution: {integrity: sha512-Se55zXv48IYLg/WzoXzpbPLcq86suwDSbRUoNb69l4dkovorqS/47Xuy7MUo/gPNwwcPB4a+aqbXbshU33dcdQ==} | |
| 1017 | 1017 | engines: {node: '>=18.0.0'} |
| 1018 | 1018 | peerDependencies: |
| 1019 | 1019 | '@anthropic-ai/sdk': '>=0.93.0' |
| @@ -10540,7 +10540,7 @@ snapshots: · 9520 unmodified lines | ||
| 10540 | 10540 | |
| 10541 | 10541 | '@alchemy.run/node-utils@0.0.5': {} |
| 10542 | 10542 | |
| 10543 | − '@anthropic-ai/claude-agent-sdk@0.3.252(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': | |
| 10543 | + '@anthropic-ai/claude-agent-sdk@0.3.257(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': | |
| 10544 | 10544 | dependencies: |
| 10545 | 10545 | '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) |
| 10546 | 10546 | '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) |