From 8298664e9e46ad8af2c8966cfb4c08a2a31898a4 Mon Sep 17 00:00:00 2001 From: maria Date: Thu, 29 Oct 2026 02:37:31 +0000 Subject: [PATCH 01/25] fix(server,cursor,opencode): refresh provider usage when a turn hits its usage limit (#26234) Co-authored-by: Claude Opus 5.5 --- .../ProviderEventIngestor.test.ts | 109 ++++++++++++++++- .../orchestration-v2/ProviderEventIngestor.ts | 21 ++++ .../ProviderSessionManager.test.ts | 5 + .../testkit/ProviderReplayHarness.ts | 5 + .../src/provider/Drivers/ClaudeDriver.ts | 1 + .../src/server/managedProvider.test.ts | 111 ++++++++++++++++++ .../src/server/managedProvider.ts | 35 ++++++ packages/provider-core/src/server/snapshot.ts | 7 +- .../src/server/CursorAgentSdk.ts | 15 +++ .../src/server/adapter.test.ts | 13 +- .../provider-cursor/src/server/adapter.ts | 8 +- .../src/server/adapter.test.ts | 45 +++++++ .../provider-opencode/src/server/adapter.ts | 6 +- 13 files changed, 373 insertions(+), 8 deletions(-) diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts index 3fecc1c7d2d61..7f1006f1c5ec5 100644 --- a/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts @@ -46,6 +46,9 @@ import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as ProviderEventIngestor from "./ProviderEventIngestor.ts"; import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts"; +import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; +import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; +import type { ProviderInstance } from "@t3tools/provider-core/server/driver"; import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; import { makeProviderEventRoutingState, @@ -68,8 +71,16 @@ const layerGit = GitVcsDriver.layer.pipe( Layer.provideMerge(NodeServices.layer), ); -const makeLayerTest = (ingestorEventSink: Layer.Layer) => +const layerEmptyInstances = Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({ + // @effect-diagnostics-next-line effectSucceedWithVoid:off - Optional instances require undefined rather than void. + getInstance: () => Effect.succeed(undefined), +}); +const makeLayerTest = ( + ingestorEventSink: Layer.Layer, + instances = layerEmptyInstances, +) => Layer.mergeAll( + instances, layerTestDatabase, layerTestStores, layerTestEventSink, @@ -79,6 +90,7 @@ const makeLayerTest = (ingestorEventSink: Layer.Layer + Effect.scoped( + Effect.gen(function* () { + const refreshed = yield* Deferred.make(); + const adapter = yield* ProviderAdapter.ProviderAdapterV2.pipe( + Effect.provide( + Layer.mock(ProviderAdapter.ProviderAdapterV2)({ + instanceId: ProviderInstanceId.make("cursor-work"), + driver: ProviderDriverKind.make("cursor"), + }), + ), + ); + const instance: ProviderInstance = { + instanceId: ProviderInstanceId.make("cursor-work"), + driverKind: ProviderDriverKind.make("cursor"), + continuationIdentity: { + driverKind: ProviderDriverKind.make("cursor"), + continuationKey: "work", + }, + enabled: true, + displayName: undefined, + orchestrationAdapter: adapter, + textGeneration: { + generateBranchName: () => Effect.die("unused"), + generateCommitMessage: () => Effect.die("unused"), + generatePrContent: () => Effect.die("unused"), + generateThreadTitle: () => Effect.die("unused"), + generateReviewGuide: () => Effect.die("unused"), + }, + snapshot: { + refreshUsage: Deferred.succeed(refreshed, undefined).pipe(Effect.asVoid), + getSnapshot: Effect.die("unused"), + refresh: Effect.die("unused"), + resolveMaintenance: () => Effect.die("unused"), + applyUsageLimits: () => Effect.die("unused"), + streamChanges: Stream.empty, + }, + }; + const lookups: ProviderInstanceId[] = []; + const instances = Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({ + getInstance: (id) => + Effect.sync(() => { + lookups.push(id); + return id === instance.instanceId ? instance : undefined; + }), + }); + yield* Effect.gen(function* () { + const now = yield* DateTime.now; + const eventSink = yield* EventSink.EventSinkV2; + const ingestor = yield* ProviderEventIngestor.ProviderEventIngestorV2; + const idAllocator = yield* IdAllocator.IdAllocatorV2; + const threadEvent = yield* threadCreatedEvent(now); + yield* eventSink.write({ events: [threadEvent] }); + const input = { + providerSessionId: yield* idAllocator.allocate.providerSession({ + providerInstanceId: instance.instanceId, + threadId: threadEvent.threadId, + }), + providerInstanceId: instance.instanceId, + threadId: threadEvent.threadId, + }; + for (const failureClass of ["validation_error", "usage_limit"] as const) { + const stored = yield* ingestor.ingestNormalized({ + ...input, + event: { + type: "turn.terminal", + driver: instance.driverKind, + providerThreadId: idAllocator.derive.providerThread({ + driver: instance.driverKind, + nativeThreadId: "limited", + }), + providerTurnId: idAllocator.derive.providerTurn({ + driver: instance.driverKind, + nativeTurnId: failureClass, + }), + runOrdinal: 1, + failureItemOrdinal: 1, + status: "failed", + failure: makeProviderFailure({ + class: failureClass, + message: "Provider rejected turn.", + }), + threadDisposition: "reusable", + }, + }); + assert.equal(stored.length, 1); + if (failureClass === "validation_error") assert.deepEqual(lookups, []); + } + assert.deepEqual(lookups, [instance.instanceId]); + yield* Deferred.await(refreshed); + }).pipe(Effect.provide(makeLayerTest(layerTestEventSink, instances))); + }), + ), +); + it.effect("records accepted billed turn usage once without billing the context window", () => { const recorded: Array>> = []; const layerAnalytics = Layer.succeed(ProviderEventIngestor.ProviderTurnAnalytics, { diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts index 6beb4d0e55471..06f444776cb3c 100644 --- a/apps/server/src/orchestration-v2/ProviderEventIngestor.ts +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts @@ -34,6 +34,7 @@ import { isTemporaryWorktreeBranch } from "@t3tools/shared/git"; import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; +import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; import * as EventSink from "./EventSink.ts"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; @@ -326,6 +327,7 @@ export const layer: Layer.Layer< | IdAllocator.IdAllocatorV2 | ProjectionStore.ProjectionStoreV2 | ThreadCommandExecutor.ThreadCommandExecutor + | ProviderInstanceRegistry.ProviderInstanceRegistry > = Layer.effect( ProviderEventIngestorV2, Effect.gen(function* () { @@ -336,6 +338,8 @@ export const layer: Layer.Layer< const threadCommands = yield* ThreadCommandExecutor.ThreadCommandExecutor; const analytics = yield* ProviderTurnAnalytics; const agentScope = yield* AgentScope; + const instances = yield* ProviderInstanceRegistry.ProviderInstanceRegistry; + const scope = yield* Effect.scope; const completedTurnAnalytics = new Set(); const makeDomainEvent = @@ -794,6 +798,23 @@ export const layer: Layer.Layer< }, (effect, input) => effect.pipe( + Effect.tap((storedEvents) => + Effect.gen(function* () { + if ( + storedEvents.length === 0 || + input.event.type !== "turn.terminal" || + input.event.status !== "failed" || + input.event.failure.class !== "usage_limit" + ) + return; + const instance = yield* instances.getInstance(input.providerInstanceId); + if (!instance?.snapshot.refreshUsage) return; + yield* instance.snapshot.refreshUsage.pipe( + Effect.ignoreCause({ log: true }), + Effect.forkIn(scope, { startImmediately: true }), + ); + }), + ), Effect.flatMap((storedEvents) => Effect.gen(function* () { if (input.event.type !== "app_thread.created") return storedEvents; diff --git a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts index 9ae9ddce81a99..cac8e4fbbb8e0 100644 --- a/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts +++ b/apps/server/src/orchestration-v2/ProviderSessionManager.test.ts @@ -47,6 +47,7 @@ import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts"; import * as ProviderEventIngestor from "./ProviderEventIngestor.ts"; +import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as ThreadCommandExecutor from "./ThreadCommandExecutor.ts"; import * as ProviderSessionManager from "./ProviderSessionManager.ts"; @@ -533,6 +534,10 @@ function layerTest(input: { const layerProviderEventIngestorTest = ProviderEventIngestor.layer.pipe( Layer.provide( Layer.mergeAll( + Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({ + // @effect-diagnostics-next-line effectSucceedWithVoid:off - Optional instances require undefined rather than void. + getInstance: () => Effect.succeed(undefined), + }), layerConfiguredEventSink, IdAllocator.layer, layerTestStores, diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts index 4da359f74c920..afd11a9ab7ecb 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts @@ -40,6 +40,7 @@ import * as ProviderAuthService from "../../provider/ProviderAuthService.ts"; import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests"; import * as ProviderContinuationService from "../ProviderContinuationService.ts"; import * as ProviderEventIngestor from "../ProviderEventIngestor.ts"; +import * as ProviderInstanceRegistry from "../../provider/ProviderInstanceRegistry.ts"; import * as ProviderRuntimeRecoveryService from "../ProviderRuntimeRecoveryService.ts"; import * as ProviderSessionManager from "../ProviderSessionManager.ts"; import * as CommandOutputRecorder from "./CommandOutputRecorder.ts"; @@ -320,6 +321,10 @@ export function layerWithRegistry( const layerProviderEventIngestorProvided = ProviderEventIngestor.layer.pipe( Layer.provide( Layer.mergeAll( + Layer.mock(ProviderInstanceRegistry.ProviderInstanceRegistry)({ + // @effect-diagnostics-next-line effectSucceedWithVoid:off - Optional instances require undefined rather than void. + getInstance: () => Effect.succeed(undefined), + }), layerStores, layerEventSinkProvided, IdAllocator.layer, diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index 86832d53d3119..e1fbcd30080fd 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -299,6 +299,7 @@ export const ClaudeDriver: ProviderDriver resolveMaintenance().pipe( Effect.flatMap((maintenanceCapabilities) => diff --git a/packages/provider-core/src/server/managedProvider.test.ts b/packages/provider-core/src/server/managedProvider.test.ts index 668502f93e432..7794107824c9b 100644 --- a/packages/provider-core/src/server/managedProvider.test.ts +++ b/packages/provider-core/src/server/managedProvider.test.ts @@ -6,6 +6,7 @@ import { type ServerProvider, } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; +import * as Cache from "effect/Cache"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -138,6 +139,116 @@ const enrichedSnapshotSecond: ServerProvider = { }; describe("makeManagedServerProvider", () => { + it.effect("invalidates cached usage and queues one refresh behind an in-flight refresh", () => + Effect.scoped( + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const probeStarted = yield* Deferred.make(); + const releaseProbe = yield* Deferred.make(); + const cache = yield* Cache.makeWith( + () => + Effect.gen(function* () { + const count = yield* Ref.updateAndGet(calls, (count) => count + 1); + if (count === 2) { + yield* Deferred.succeed(probeStarted, undefined); + yield* Deferred.await(releaseProbe); + } + return { + ...refreshedSnapshot, + usageLimits: { + checkedAt: refreshedSnapshot.checkedAt, + windows: [ + { + id: "five_hour", + kind: "session", + label: "Session", + usedPercent: count === 1 ? 20 : 100, + }, + ], + }, + } satisfies ServerProvider; + }), + { capacity: 1, timeToLive: () => "5 minutes" }, + ); + const warmed = yield* Cache.get(cache, "claude"); + const provider = yield* makeManagedServerProvider({ + resolveMaintenance: () => Effect.die("usage refresh must not resolve maintenance"), + getSettings: Effect.succeed({ enabled: true }), + streamSettings: Stream.empty, + haveSettingsChanged: () => false, + initialSnapshot: () => Effect.succeed(warmed), + checkProvider: Cache.get(cache, "claude"), + invalidateUsageCache: Cache.invalidateAll(cache), + refreshOnInterval: false, + }); + yield* provider.streamChanges.pipe(Stream.take(1), Stream.runDrain); + assert.strictEqual(yield* Ref.get(calls), 1); + const refresh = yield* provider.refreshUsage!.pipe(Effect.forkChild); + yield* Deferred.await(probeStarted); + const queued = yield* provider.refreshUsage!.pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* provider.refreshUsage!; + assert.strictEqual(yield* Ref.get(calls), 2); + assert.strictEqual((yield* provider.getSnapshot).usageLimits?.windows[0]?.usedPercent, 20); + yield* Deferred.succeed(releaseProbe, undefined); + yield* Fiber.join(refresh); + yield* Fiber.join(queued); + assert.strictEqual(yield* Ref.get(calls), 3); + assert.strictEqual((yield* provider.getSnapshot).usageLimits?.windows[0]?.usedPercent, 100); + }), + ).pipe(Effect.provide(layerAlwaysRunTest)), + ); + it.effect("keeps the waiting usage refresh's claim when the running one is interrupted", () => + Effect.scoped( + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const usageProbeStarted = yield* Deferred.make(); + const statusProbeStarted = yield* Deferred.make(); + const releaseStatusProbe = yield* Deferred.make(); + const provider = yield* makeManagedServerProvider({ + resolveMaintenance: () => Effect.succeed(maintenanceCapabilities), + getSettings: Effect.succeed({ enabled: true }), + streamSettings: Stream.empty, + haveSettingsChanged: () => false, + initialSnapshot: () => Effect.succeed(initialSnapshot), + checkProvider: Effect.gen(function* () { + const count = yield* Ref.updateAndGet(calls, (count) => count + 1); + if (count === 2) { + yield* Deferred.succeed(usageProbeStarted, undefined); + return yield* Effect.never; + } + if (count === 3) { + yield* Deferred.succeed(statusProbeStarted, undefined); + yield* Deferred.await(releaseStatusProbe); + } + return { + ...refreshedSnapshot, + usageLimits: { checkedAt: refreshedSnapshot.checkedAt, windows: [] }, + } satisfies ServerProvider; + }), + refreshOnInterval: false, + }); + yield* provider.streamChanges.pipe(Stream.take(1), Stream.runDrain); + const running = yield* provider.refreshUsage!.pipe(Effect.forkChild); + yield* Deferred.await(usageProbeStarted); + const status = yield* provider.refresh.pipe(Effect.forkChild({ startImmediately: true })); + const waiting = yield* provider.refreshUsage!.pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* Fiber.interrupt(running); + yield* Deferred.await(statusProbeStarted); + const extra = yield* provider.refreshUsage!.pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* Deferred.succeed(releaseStatusProbe, undefined); + yield* Fiber.join(status); + yield* Fiber.join(waiting); + yield* Fiber.join(extra); + assert.strictEqual(yield* Ref.get(calls), 4); + }), + ).pipe(Effect.provide(layerAlwaysRunTest)), + ); it.effect( "runs the initial provider check in the background and streams the refreshed snapshot", () => diff --git a/packages/provider-core/src/server/managedProvider.ts b/packages/provider-core/src/server/managedProvider.ts index f40baee525c8d..41ff65b0f294b 100644 --- a/packages/provider-core/src/server/managedProvider.ts +++ b/packages/provider-core/src/server/managedProvider.ts @@ -44,6 +44,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean; readonly initialSnapshot: (settings: Settings) => Effect.Effect; readonly checkProvider: Effect.Effect; + readonly invalidateUsageCache?: Effect.Effect; readonly enrichSnapshot?: (input: { readonly settings: Settings; readonly snapshot: ServerProvider; @@ -60,6 +61,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( > { const host = yield* ProviderHost.ProviderHost; const refreshSemaphore = yield* Semaphore.make(1); + const usageRefreshQueued = yield* Ref.make(null); const changesPubSub = yield* Effect.acquireRelease( PubSub.unbounded(), PubSub.shutdown, @@ -216,6 +218,38 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( return yield* applySnapshot(nextSettings, { forceRefresh: true }); }); + const refreshUsage = Effect.fn("ManagedServerProvider.refreshUsage")(function* () { + const { snapshot } = yield* Ref.get(snapshotStateRef); + if ( + !snapshot.enabled || + !snapshot.usageLimits || + snapshot.usageLimits.unavailable?.reason === "unsupported" + ) + return; + // An in-flight refresh may have read cached usage, so one refresh waits behind it. + const claim = Symbol(); + const release = Ref.update(usageRefreshQueued, (queued) => (queued === claim ? null : queued)); + yield* Effect.uninterruptibleMask((restore) => + Ref.modify(usageRefreshQueued, (queued) => [queued !== null, queued ?? claim]).pipe( + Effect.flatMap((held) => + held + ? Effect.void + : restore( + refreshSemaphore.withPermits(1)( + release.pipe( + Effect.andThen(input.invalidateUsageCache ?? Effect.void), + Effect.andThen(input.getSettings), + Effect.flatMap((settings) => + applySnapshotBase(settings, { forceRefresh: true }), + ), + ), + ), + ).pipe(Effect.onInterrupt(() => release)), + ), + ), + ); + }); + const shouldRefreshOnInterval = Effect.gen(function* () { const state = yield* Ref.get(snapshotStateRef); // A disabled instance has nothing to probe; a periodic check would only @@ -304,6 +338,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( .resolveMaintenance({ fresh: true }) .pipe(Effect.andThen(refreshSnapshot), Effect.tapError(Effect.logError), Effect.orDie), applyUsageLimits, + refreshUsage: refreshUsage(), get streamChanges() { return Stream.fromPubSub(changesPubSub); }, diff --git a/packages/provider-core/src/server/snapshot.ts b/packages/provider-core/src/server/snapshot.ts index c9c877d29fe20..88f2c1f113848 100644 --- a/packages/provider-core/src/server/snapshot.ts +++ b/packages/provider-core/src/server/snapshot.ts @@ -1,4 +1,8 @@ -import type { ProviderUsageLimitsUpdate, ServerProvider } from "@t3tools/contracts"; +import type { + ProviderUsageLimitsUpdate, + ServerProvider, + ServerSettingsError, +} from "@t3tools/contracts"; import type * as Effect from "effect/Effect"; import type * as Stream from "effect/Stream"; import type { ProviderMaintenanceCapabilities } from "./maintenance.ts"; @@ -10,6 +14,7 @@ export interface ManagedServerProvider { }) => Effect.Effect; readonly getSnapshot: Effect.Effect; readonly refresh: Effect.Effect; + readonly refreshUsage?: Effect.Effect; readonly streamChanges: Stream.Stream; /** Merge windows by ID; updates with no usable window leave the snapshot untouched. */ readonly applyUsageLimits: ( diff --git a/packages/provider-cursor/src/server/CursorAgentSdk.ts b/packages/provider-cursor/src/server/CursorAgentSdk.ts index 7f0decb49af3f..5986d09506d09 100644 --- a/packages/provider-cursor/src/server/CursorAgentSdk.ts +++ b/packages/provider-cursor/src/server/CursorAgentSdk.ts @@ -43,6 +43,21 @@ export function cursorSdkFailureMessage(error: CursorAgentSdkRunnerError): strin : `Cursor Agent SDK ${error.method} failed: ${reason}`; } +// The SDK maps backend details to codes like PRO_USER_USAGE_LIMIT or RATE_LIMITED, without a status. +const CURSOR_USAGE_LIMIT_CODE = + /^(?:resource_exhausted|rate_limited(?:_changeable)?|\w*_usage_limit|\w*_rate_limit(?:_exceeded)?)$/i; + +export function isCursorUsageLimitError(cause: unknown): boolean { + const error = isCursorAgentSdkRunnerError(cause) ? cause.cause : cause; + if (typeof error !== "object" || error === null) return false; + const code = Reflect.get(error, "code"); + return ( + Reflect.get(error, "name") === "RateLimitError" || + Reflect.get(error, "status") === 429 || + (typeof code === "string" && CURSOR_USAGE_LIMIT_CODE.test(code)) + ); +} + const GENERIC_SDK_ERROR_CODES = new Set(["", "error", "unknown"]); function sdkErrorReason(cause: unknown): string | undefined { diff --git a/packages/provider-cursor/src/server/adapter.test.ts b/packages/provider-cursor/src/server/adapter.test.ts index d91d3bc339933..062089fffa0c3 100644 --- a/packages/provider-cursor/src/server/adapter.test.ts +++ b/packages/provider-cursor/src/server/adapter.test.ts @@ -214,6 +214,15 @@ describe("CursorAdapterV2", () => { ); it.effect.each([ + { + reason: "the SDK's detailed plan usage limit code", + wait: Effect.succeed({ + id: "native-cursor-run", + status: "error" as const, + error: { message: "You've hit your usage limit", code: "PRO_USER_USAGE_LIMIT" }, + }), + failure: { class: "usage_limit", message: "You've hit your usage limit" }, + }, { reason: "the error Cursor's run ended with", wait: Effect.succeed({ @@ -232,7 +241,7 @@ describe("CursorAdapterV2", () => { }, }, { - reason: "the SDK error that broke the run", + reason: "the SDK rate limit that broke the run", wait: Effect.fail( new CursorAgentSdk.CursorAgentSdkRunnerError({ method: "run.wait", @@ -243,7 +252,7 @@ describe("CursorAdapterV2", () => { }), ), failure: { - class: "transport_error", + class: "usage_limit", message: "Cursor Agent SDK run.wait failed: Too many requests (resource_exhausted, HTTP 429)", }, diff --git a/packages/provider-cursor/src/server/adapter.ts b/packages/provider-cursor/src/server/adapter.ts index 93652e492e031..72aca919b25f4 100644 --- a/packages/provider-cursor/src/server/adapter.ts +++ b/packages/provider-cursor/src/server/adapter.ts @@ -2506,7 +2506,9 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( cause: result.error, // makeProviderFailure never reads a cause's own text. message: result.error?.message, - class: "provider_error", + class: CursorAgentSdk.isCursorUsageLimitError(result.error) + ? "usage_limit" + : "provider_error", }) : makeProviderFailure({ cause: transportFailure, @@ -2532,7 +2534,9 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* ( message: CursorAgentSdk.isCursorAgentSdkRunnerError(cause) ? CursorAgentSdk.cursorSdkFailureMessage(cause) : undefined, - class: "transport_error", + class: CursorAgentSdk.isCursorUsageLimitError(cause) + ? "usage_limit" + : "transport_error", }), }), }); diff --git a/packages/provider-opencode/src/server/adapter.test.ts b/packages/provider-opencode/src/server/adapter.test.ts index 759545fff0d19..41da6bd0ff435 100644 --- a/packages/provider-opencode/src/server/adapter.test.ts +++ b/packages/provider-opencode/src/server/adapter.test.ts @@ -240,6 +240,51 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct }); describe("OpenCodeAdapterV2", () => { + it.effect("classifies APIError 429 as a usage limit", () => + Effect.gen(function* () { + const nativeEvents = asyncEventStream(); + const harness = yield* makeOpenCodeRuntimeHarness("usage-limit", "root", { + event: { subscribe: async () => ({ stream: nativeEvents.stream }) }, + session: { + create: async () => ({ data: { id: "root", time: { created: 1, updated: 1 } } }), + promptAsync: async () => ({ data: true }), + abort: async () => ({ data: true }), + children: async () => ({ data: [] }), + }, + }); + yield* harness.startTurn(); + const received = yield* harness.runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + Effect.forkScoped, + ); + yield* Effect.promise(() => + nativeEvents.push({ + type: "session.error", + properties: { + sessionID: "root", + error: { + name: "APIError", + data: { + message: "Provider rejected request", + statusCode: 429, + isRetryable: true, + }, + }, + }, + }), + ); + const terminal = (yield* Fiber.join(received)).find( + (event) => event.type === "turn.terminal", + ); + assert.equal(terminal?.status, "failed"); + assert.equal(terminal?.failure?.class, "usage_limit"); + }).pipe( + Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)), + Effect.scoped, + ), + ); + it.effect("applies a changed runtime policy to the session on resume and turn start", () => Effect.gen(function* () { const updates: Array<{ sessionID: string; permission: unknown }> = []; diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index ef85a7e70b29b..2a851188d076e 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -2643,6 +2643,8 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function (state): state is OpenCodeThreadState => state !== undefined, ); const message = openCodeErrorMessage(event); + const error = event.properties.error; + const usageLimit = error?.name === "APIError" && error.data.statusCode === 429; if ( !isMessageAbortedError(event) && (event.properties.sessionID === undefined || @@ -2659,8 +2661,8 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function { failure: makeProviderFailure({ message, - code: event.properties.error?.name ?? null, - class: "provider_error", + code: error?.name ?? null, + class: usageLimit ? "usage_limit" : "provider_error", }), threadDisposition: event.properties.sessionID === undefined ? "broken" : "reusable", From fe219c4de18f5ee37c28cbd3914a5569b1579156 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:29:07 +0530 Subject: [PATCH 02/25] fix(web): keep tool groups collapsed after a usage-limit pause (#24937) Co-authored-by: PR Batch Tester Co-authored-by: Claude Opus 5.5 --- .../chat/MessagesTimeline.logic.test.ts | 74 +++++++++++-------- .../components/chat/MessagesTimeline.logic.ts | 23 +++++- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index 3ee079809f14e..8f4b15cc11f91 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -3841,9 +3841,15 @@ it("keeps the working header in place across worktree setup handoff", () => { }); describe("failed turn transcript", () => { - it.each(["provider_error", "usage_limit"] as const)( - "keeps historical %s failures and preceding work visible without disclosures", - (failureClass) => { + it.each([ + { position: "historical", failureClass: "provider_error" }, + { position: "historical", failureClass: "usage_limit" }, + { position: "latest", failureClass: "usage_limit" }, + ] as const)( + "keeps $position $failureClass failures visible with preceding work grouped appropriately", + ({ position, failureClass }) => { + const isLatest = position === "latest"; + const commands = isLatest ? ["pwd", "ls", "false"] : ["pwd"]; const runId = RunId.make("failed-run"); const threadId = ThreadId.make("failed-thread"); const at = DateTime.makeUnsafe("2026-09-20T12:00:00Z"); @@ -3880,23 +3886,24 @@ describe("failed turn transcript", () => { attachments: [], }, }, - { - position: 1, + ...commands.map((input, index): OrchestrationV2ProjectedTurnItem => ({ + position: index + 1, visibility: "local", sourceThreadId: threadId, - sourceItemId: TurnItemId.make("command"), + sourceItemId: TurnItemId.make(index === 0 ? "command" : `command-${index}`), item: { ...base, - id: TurnItemId.make("command"), + id: TurnItemId.make(index === 0 ? "command" : `command-${index}`), + ordinal: index + 1, type: "command_execution", status: "completed", - input: "pwd", + input, output: "", exitCode: 0, }, - }, + })), { - position: 2, + position: commands.length + 1, visibility: "local", sourceThreadId: threadId, sourceItemId: TurnItemId.make("failure"), @@ -3918,20 +3925,24 @@ describe("failed turn transcript", () => { timelineEntries: deriveTimelineEntriesFromVisibleTurnItems({ visibleTurnItems: items, optimisticMessages: [], - }).map((entry) => ({ - ...entry, - attempt: { - id: RunAttemptId.make("superseded-attempt"), - runId, - attemptOrdinal: 1, - rootNodeId: NodeId.make("superseded-root"), - status: "superseded" as const, - completedAt: null, - }, - })), + }).map((entry) => + isLatest + ? entry + : { + ...entry, + attempt: { + id: RunAttemptId.make("superseded-attempt"), + runId, + attemptOrdinal: 1, + rootNodeId: NodeId.make("superseded-root"), + status: "superseded" as const, + completedAt: null, + }, + }, + ), latestRun: { - runId: RunId.make("newer-run"), - status: "completed", + runId: isLatest ? runId : RunId.make("newer-run"), + status: isLatest ? "failed" : "completed", startedAt: DateTime.formatIso(at), completedAt: DateTime.formatIso(at), }, @@ -3939,16 +3950,19 @@ describe("failed turn transcript", () => { turnDiffSummaries: [], supportsConversationRollback: false, }); - expect( - rows.some( - (row) => - row.kind === "turn-fold" || row.kind === "attempt-fold" || row.kind === "work-toggle", - ), - ).toBe(false); + expect(rows.some((row) => row.kind === "turn-fold" || row.kind === "attempt-fold")).toBe( + false, + ); const work = rows.flatMap((row) => (row.kind === "work" ? row.groupedEntries : [])); - expect(work.map((entry) => entry.id)).toEqual(["command", "failure"]); + expect(rows.find((row) => row.kind === "work-toggle")).toEqual( + isLatest ? expect.objectContaining({ hiddenCount: 3, expanded: false }) : undefined, + ); + expect(work.map((entry) => entry.id)).toEqual( + isLatest ? ["failure"] : ["command", "failure"], + ); expect(work.at(-1)?.detail).toBe("The provider stopped this turn.\nRetry later."); expect(work.at(-1)?.createdAt).toBe("2026-09-20T12:00:00.000Z"); + if (isLatest) return; const entriesWithoutTools = deriveTimelineEntriesFromVisibleTurnItems({ visibleTurnItems: items.filter((row) => row.item.type !== "command_execution"), optimisticMessages: [], diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index f4b409f901b53..c26bcf51a718a 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -973,6 +973,24 @@ function failedTimelineRunIds( return failed; } +/** Usage-limit pauses keep their tool groups; any other failure itemizes the run's work. */ +function itemizedFailedRunIds( + entries: ReadonlyArray, + failedRunIds: ReadonlySet, +) { + const usageLimited = new Set(); + const otherFailures = new Set(); + for (const entry of entries) { + const item = timelineEntryFailedItem(entry); + if (item?.runId) { + (item.failure.class === "usage_limit" ? usageLimited : otherFailures).add(item.runId); + } + } + return new Set( + [...failedRunIds].filter((runId) => !usageLimited.has(runId) || otherFailures.has(runId)), + ); +} + function deriveTurnFolds(input: { timelineEntries: ReadonlyArray; terminalAssistantMessageIds: ReadonlySet; @@ -1410,6 +1428,7 @@ export function deriveMessagesTimelineRows(input: { const nextRows: MessagesTimelineRow[] = []; const terminalAssistantMessageIds = deriveTerminalAssistantMessageIds(timelineEntries); const failedRunIds = failedTimelineRunIds(timelineEntries, input.latestRun ?? null); + const itemizedRunIds = itemizedFailedRunIds(timelineEntries, failedRunIds); const liveSubagentEntryIds = liveSubagentCardEntryIds(timelineEntries); const completeSupersededReplyIds = completeSupersededReplyEntryIds(timelineEntries); const supersededFoldsByAnchorEntryId = deriveSupersededAttemptFolds( @@ -1772,7 +1791,7 @@ export function deriveMessagesTimelineRows(input: { if (timelineEntry.kind === "work") { if ( isPendingApproval(timelineEntry.entry) || - (timelineEntry.entry.runId != null && failedRunIds.has(timelineEntry.entry.runId)) || + (timelineEntry.entry.runId != null && itemizedRunIds.has(timelineEntry.entry.runId)) || (timelineEntry.entry.itemType === "error" && timelineEntry.entry.toolLifecycleStatus === "failed") || timelineEntry.entry.tone === "error" || @@ -1798,6 +1817,8 @@ export function deriveMessagesTimelineRows(input: { !nextEntry || nextEntry.kind !== "work" || nextEntry.entry.sourceActivityKind === "context-compaction" || + (nextEntry.entry.itemType === "error" && + nextEntry.entry.toolLifecycleStatus === "failed") || nextEntry.entry.tone === "error" || nextEntry.entry.sourceActivityKind === "runtime.error" || nextEntry.entry.itemType === "system_notice" || From 7632c3dc7039953cc50282e1bc339e1bf11f2ffc Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:14:44 +0530 Subject: [PATCH 03/25] fix(mobile): photo-library PNGs no longer fail after resizing (#26522) Co-authored-by: PR Batch Tester Co-authored-by: Claude Opus 5.5 --- apps/mobile/src/lib/composerFiles.test.ts | 60 ++++++++++++++++++++--- apps/mobile/src/lib/composerImageFit.ts | 59 ++++++++++++++++------ 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/apps/mobile/src/lib/composerFiles.test.ts b/apps/mobile/src/lib/composerFiles.test.ts index 546c28f408f78..bdcd35c36ff75 100644 --- a/apps/mobile/src/lib/composerFiles.test.ts +++ b/apps/mobile/src/lib/composerFiles.test.ts @@ -124,28 +124,35 @@ describe("composer file attachments", () => { width: 4032, height: 3024, }; + type Size = { width: number; height: number }; const native = { - size: { width: 1, height: 1 }, - resizes: [] as Array<{ width?: number | null; height?: number | null }>, + size: { width: 1, height: 1 } as Size, + resizes: [] as Size[], saved: rendered as { uri: string; base64?: string }, + encode: null as ((size: Size, format: string) => { uri: string; base64?: string }) | null, }; beforeEach(() => { native.size = { width: 1, height: 1 }; native.resizes = []; native.saved = rendered; + native.encode = null; mocks.manipulate.mockReset(); mocks.release.mockReset(); - mocks.manipulate.mockImplementation(() => { + mocks.manipulate.mockImplementation((source: string | Size) => { + let size = typeof source === "string" ? native.size : source; const context = { - resize(size: { width?: number | null; height?: number | null }) { - native.resizes.push(size); + resize(next: Size) { + native.resizes.push(next); + size = next; return context; }, renderAsync: async () => ({ - ...native.size, + width: size.width, + height: size.height, release: mocks.release, - saveAsync: async () => native.saved, + saveAsync: async (options: { format: string }) => + native.encode?.(size, options.format) ?? native.saved, }), }; return context; @@ -210,6 +217,45 @@ describe("composer file attachments", () => { ]); }); + it("shrinks a fitted PNG further when re-encoding it exceeds the image limit", async () => { + const header = Buffer.alloc(24); + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]).copy(header); + header.write("IHDR", 12); + header.writeUInt32BE(1668, 16); + header.writeUInt32BE(2388, 20); + native.size = { width: 1668, height: 2388 }; + // Native PNG encoding of the 2000px resample is larger than the 7 MB original. + native.encode = (size, format) => ({ + uri: `file:///cache/fitted-${size.width}x${size.height}.${format}`, + base64: Buffer.alloc(size.width * size.height * 4).toString("base64"), + }); + mocks.size.mockReturnValue(7 * 1024 * 1024); + mocks.readBase64.mockResolvedValue(header.toString("base64")); + mocks.pickMedia.mockResolvedValue({ + canceled: false, + assets: [{ ...photo, fileName: "IMG_9280.png", mimeType: "image/png" }], + }); + + const result = await pickComposerImages({ existingCount: 0 }); + + expect(result.error).toBeNull(); + const [image] = result.images; + const last = native.resizes.at(-1)!; + expect(native.resizes[0]).toEqual({ width: 1397, height: 2000 }); + expect(Math.max(last.width, last.height)).toBeLessThan(2000); + expect(image).toEqual( + expect.objectContaining({ + name: "IMG_9280.png", + mimeType: "image/png", + sizeBytes: last.width * last.height * 4, + previewUri: `file:///cache/fitted-${last.width}x${last.height}.png`, + }), + ); + expect(image!.sizeBytes).toBeLessThanOrEqual(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES); + expect(mocks.delete).toHaveBeenCalledWith("file:///cache/fitted-1397x2000.png"); + expect(mocks.delete).not.toHaveBeenCalledWith(image!.previewUri); + }); + it("measures the picker file instead of trusting the reported size", async () => { mocks.pickMedia.mockResolvedValue({ canceled: false, diff --git a/apps/mobile/src/lib/composerImageFit.ts b/apps/mobile/src/lib/composerImageFit.ts index fddeac277bcfe..18cd93b3fe723 100644 --- a/apps/mobile/src/lib/composerImageFit.ts +++ b/apps/mobile/src/lib/composerImageFit.ts @@ -1,3 +1,4 @@ +import { PROVIDER_SEND_TURN_MAX_IMAGE_BYTES } from "@t3tools/contracts"; import { downscalableImageMimeType, fitImageAttachmentSize, @@ -6,8 +7,24 @@ import { IMAGE_DIMENSIONS_HEADER_BYTES, readImageDimensions, } from "@t3tools/shared/imageDimensions"; +import { estimateBase64ByteSize } from "./base64"; const PHOTO_JPEG_QUALITY = 0.85; +const MAX_BYTE_LIMIT_SHRINKS = 3; + +function shrinkToByteLimit( + size: { readonly width: number; readonly height: number }, + bytes: number, +): { width: number; height: number } | null { + if (bytes <= PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) return null; + // Encoded size tracks pixel count, so scale each edge by the square root with some headroom. + const scale = Math.sqrt(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / bytes) * 0.9; + const next = { + width: Math.max(1, Math.floor(size.width * scale)), + height: Math.max(1, Math.floor(size.height * scale)), + }; + return next.width < size.width || next.height < size.height ? next : null; +} export async function renderPhoto( uri: string, @@ -16,21 +33,35 @@ export async function renderPhoto( const { ImageManipulator, SaveFormat } = await import("expo-image-manipulator"); let image = await ImageManipulator.manipulate(uri).renderAsync(); try { - const fittedSize = fitImageAttachmentSize(image); - if (fittedSize !== null) { - const resized = await ImageManipulator.manipulate(image).resize(fittedSize).renderAsync(); - image.release(); - image = resized; - } - const saved = await image.saveAsync({ - format: format === "png" ? SaveFormat.PNG : SaveFormat.JPEG, - compress: PHOTO_JPEG_QUALITY, - base64: true, - }); - if (!saved.base64) { - throw new Error("The rendered photo has no bytes."); + let size = fitImageAttachmentSize(image); + for (let shrinks = 0; ; shrinks += 1) { + if (size !== null) { + const resized = await ImageManipulator.manipulate(image).resize(size).renderAsync(); + image.release(); + image = resized; + } + const saved = await image.saveAsync({ + format: format === "png" ? SaveFormat.PNG : SaveFormat.JPEG, + compress: PHOTO_JPEG_QUALITY, + base64: true, + }); + if (!saved.base64) { + throw new Error("The rendered photo has no bytes."); + } + // PNG ignores compress, so a resampled PNG can encode larger than its original. + size = + shrinks < MAX_BYTE_LIMIT_SHRINKS + ? shrinkToByteLimit(image, estimateBase64ByteSize(saved.base64)) + : null; + if (size === null) return { base64: saved.base64, uri: saved.uri }; + try { + const { File } = await import("expo-file-system"); + const discarded = new File(saved.uri); + if (discarded.exists) discarded.delete(); + } catch (error) { + console.warn("[composer-images] could not remove discarded render", error); + } } - return { base64: saved.base64, uri: saved.uri }; } finally { image.release(); } From ab1e221618724177d67ff3d2cf07e5b211a0bfc2 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 29 Oct 2026 10:24:38 +0530 Subject: [PATCH 04/25] fix(server): guest links no longer return file diffs (#26538) --- .../src/guest/GuestShareService.test.ts | 51 +++++++++++++++---- apps/server/src/guest/GuestShareService.ts | 8 ++- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/apps/server/src/guest/GuestShareService.test.ts b/apps/server/src/guest/GuestShareService.test.ts index 31acd7bdb60bf..1ef25744c8c55 100644 --- a/apps/server/src/guest/GuestShareService.test.ts +++ b/apps/server/src/guest/GuestShareService.test.ts @@ -411,7 +411,6 @@ it.effect("serves no render for an item outside the shared thread", () => }).pipe(Effect.provide(layerTest)), ); -/** A message from the owner, which opens a run for the command below. */ const ownerRun = Effect.gen(function* () { const orchestrator = yield* Orchestrator.OrchestratorV2; yield* orchestrator.dispatch({ @@ -430,12 +429,17 @@ const ownerRun = Effect.gen(function* () { return runs[0]!.id; }); -const writeCommand = ( +const writeItem = ( id: string, - fields: Pick< - Extract, - "status" | "output" - >, + fields: + | Pick< + Extract, + "type" | "status" | "input" | "output" + > + | Pick< + Extract, + "type" | "status" | "fileName" | "diffStr" | "oldStr" | "newStr" + >, ) => Effect.gen(function* () { const runId = yield* ownerRun; @@ -464,8 +468,6 @@ const writeCommand = ( startedAt: now, completedAt: fields.status === "running" ? null : now, updatedAt: now, - type: "command_execution", - input: "ls /work/src", ...fields, }, }, @@ -474,17 +476,40 @@ const writeCommand = ( return TurnItemId.make(id); }); -it.effect("gives the guest a command's detail and output, as the owner's row loads them", () => +it.effect("gives the guest a command's detail and output, but no file diffs", () => Effect.gen(function* () { const { guestShares, credential } = yield* shareThread; const { token: session } = yield* guestShares.redeem({ credential, name: "Sam" }); - const command = yield* writeCommand("item:command", { + const command = yield* writeItem("item:command", { + type: "command_execution", status: "completed", + input: "ls /work/src", output: "/work/src/auth.ts\n", }); const detail = (yield* guestShares.item(session, command)).item; assert.deepInclude(detail, { input: "ls /work/src", output: "/work/src/auth.ts\n" }); + const edit = yield* writeItem("item:edit", { + type: "file_change", + status: "completed", + fileName: "src/auth.ts", + diffStr: "-const token = 1;\n+const token = 2;\n", + }); + const write = yield* writeItem("item:write", { + type: "file_change", + status: "completed", + fileName: "src/config.ts", + oldStr: "", + newStr: "export const key = 1;\n", + }); + for (const [id, fileName] of [ + [edit, "src/auth.ts"], + [write, "src/config.ts"], + ] as const) { + const change = (yield* guestShares.item(session, id)).item; + assert.deepInclude(change, { type: "file_change", fileName }); + assert.doesNotHaveAnyKeys(change, ["diffStr", "oldStr", "newStr"]); + } const frames = Array.from( yield* Stream.runCollect(yield* guestShares.commandOutput(session, command)), ); @@ -508,7 +533,11 @@ it.effect("ends a running command's output once the owner stops sharing", () => Effect.gen(function* () { const { orchestrator, guestShares, credential } = yield* shareThread; const { token: session } = yield* guestShares.redeem({ credential, name: "Sam" }); - const command = yield* writeCommand("item:running", { status: "running" }); + const command = yield* writeItem("item:running", { + type: "command_execution", + status: "running", + input: "ls /work/src", + }); const collecting = yield* Stream.runCollect( yield* guestShares.commandOutput(session, command), ).pipe(Effect.forkChild); diff --git a/apps/server/src/guest/GuestShareService.ts b/apps/server/src/guest/GuestShareService.ts index 0346068bc546f..347bdc11ece5b 100644 --- a/apps/server/src/guest/GuestShareService.ts +++ b/apps/server/src/guest/GuestShareService.ts @@ -57,6 +57,7 @@ import { buildBoundedThreadStreamSnapshot } from "../orchestration-v2/ThreadStre import { projectDomainEventForWire, projectTurnItemForDetail, + projectTurnItemForWire, } from "../orchestration-v2/WireProjection.ts"; import * as ProviderRegistry from "../provider/ProviderRegistry.ts"; import * as ServerSettings from "../serverSettings.ts"; @@ -179,7 +180,7 @@ export class GuestShareService extends Context.Service< session: string, itemId: string, ) => Effect.Effect<{ readonly path: string; readonly title: string }, GuestShareAccessError>; - /** One item of the shared thread, as the owner's expanded row loads it. */ + /** One item of the shared thread, as the owner's expanded row loads it, without file diffs. */ readonly item: ( session: string, itemId: string, @@ -658,7 +659,10 @@ const make = Effect.gen(function* () { const item = Effect.fn("GuestShareService.item")(function* (session: string, itemId: string) { const { item } = yield* threadItem(session, itemId); - return { item: projectTurnItemForDetail(item) }; + return { + item: + item.type === "file_change" ? projectTurnItemForWire(item) : projectTurnItemForDetail(item), + }; }); const commandOutput = Effect.fn("GuestShareService.commandOutput")(function* ( From eb565e13abebe1a71736b4651e4131f52b1950ba Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 29 Oct 2026 10:24:48 +0530 Subject: [PATCH 05/25] fix(claude): Open diff shows Claude edits as a diff instead of raw JSON (#26529) --- apps/server/package.json | 1 + .../Adapters/ClaudeAdapterV2.test.ts | 146 ++++++++++++++++++ .../Adapters/ClaudeAdapterV2.ts | 43 +++++- pnpm-lock.yaml | 3 + 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/apps/server/package.json b/apps/server/package.json index 5abc769412d1a..2735d71c6533b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -58,6 +58,7 @@ "@t3tools/source-control-github": "workspace:*", "@t3tools/source-control-gitlab": "workspace:*", "character-entities": "2.0.2", + "diff": "8.0.3", "effect": "catalog:", "jose": "catalog:", "node-pty": "^1.2.0-beta.15", diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 47457be660998..ea5ee249a787f 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -8740,6 +8740,152 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect.each([ + { + case: "an Edit", + toolName: "Edit", + toolInput: { + file_path: "/workspace/src/math.ts", + old_string: " return a - b;", + new_string: " return a + b;", + }, + toolUseResult: { + filePath: "/workspace/src/math.ts", + oldString: " return a - b;", + newString: " return a + b;", + originalFile: "export function add(a: number, b: number) {\n return a - b;\n}\n", + structuredPatch: [ + { + oldStart: 1, + oldLines: 3, + newStart: 1, + newLines: 3, + lines: [ + " export function add(a: number, b: number) {", + "- return a - b;", + "+ return a + b;", + " }", + ], + }, + ], + userModified: false, + replaceAll: false, + }, + diffStr: + "--- /workspace/src/math.ts\n+++ /workspace/src/math.ts\n@@ -1,3 +1,3 @@\n export function add(a: number, b: number) {\n- return a - b;\n+ return a + b;\n }\n", + }, + { + case: "a Write that creates a file", + toolName: "Write", + toolInput: { file_path: "/workspace/notes.md", content: "# Notes\n\nShip it.\n" }, + toolUseResult: { + type: "create", + filePath: "/workspace/notes.md", + content: "# Notes\n\nShip it.\n", + structuredPatch: [], + originalFile: null, + userModified: false, + }, + diffStr: "--- /dev/null\n+++ /workspace/notes.md\n@@ -0,0 +1,3 @@\n+# Notes\n+\n+Ship it.\n", + }, + { + case: "a Write that creates an empty file", + toolName: "Write", + toolInput: { file_path: "/workspace/.keep", content: "" }, + toolUseResult: { + type: "create", + filePath: "/workspace/.keep", + content: "", + structuredPatch: [], + originalFile: null, + userModified: false, + }, + diffStr: undefined, + }, + ])("stores the diff panel patch for $case", (testCase) => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + const toolUseId = `toolu_01${testCase.toolName}Patch`; + + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make(`attempt-claude-${testCase.toolName}-patch`), + text: "Change it.", + attachments: [], + }), + ); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "assistant", + message: { + model: "claude-sonnet-4-6", + id: `msg_${testCase.toolName}_patch`, + type: "message", + role: "assistant", + content: [ + { + type: "tool_use", + id: toolUseId, + name: testCase.toolName, + input: testCase.toolInput, + }, + ], + }, + parent_tool_use_id: null, + uuid: "00000000-0000-4000-8000-000000000793", + session_id: WAKE_NATIVE_SESSION, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + claudeSdkFrame({ + type: "user", + message: { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: toolUseId, + content: `The file ${testCase.toolInput.file_path} has been updated successfully.`, + }, + ], + }, + parent_tool_use_id: null, + uuid: "00000000-0000-4000-8000-000000000794", + session_id: WAKE_NATIVE_SESSION, + tool_use_result: testCase.toolUseResult, + }), + ); + yield* Queue.offer( + harness.sdkMessages, + makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000795", result: "Done." }), + ); + yield* harness.awaitTerminal(1); + + const fileChange = harness.events.findLast( + (event) => + event.type === "turn_item.updated" && + event.turnItem.nativeItemRef?.nativeId === toolUseId, + ); + assert.deepEqual( + fileChange?.type === "turn_item.updated" && fileChange.turnItem.type === "file_change" + ? { diffStr: fileChange.turnItem.diffStr } + : undefined, + { diffStr: testCase.diffStr }, + ); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("answers an approval a held wake turn raises without waiting for the echo", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index dd4fc8f30af92..b1273cee72061 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -106,6 +106,7 @@ import { resolveIdleCompaction, } from "@t3tools/contracts"; +import { FILE_HEADERS_ONLY, formatPatch, structuredPatch } from "diff"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -2965,6 +2966,44 @@ function claudeNativeToolOutputText(output: ClaudeNativeToolOutput): string { return typeof value === "string" ? value : value === undefined ? "" : jsonStringifyForTool(value); } +const decodeClaudeFileEditResult = Schema.decodeUnknownOption( + Schema.Struct({ + type: Schema.optional(Schema.String), + filePath: Schema.String, + content: Schema.optional(Schema.String), + structuredPatch: Schema.mutable( + Schema.Array( + Schema.Struct({ + oldStart: Schema.Number, + oldLines: Schema.Number, + newStart: Schema.Number, + newLines: Schema.Number, + lines: Schema.mutable(Schema.Array(Schema.String)), + }), + ), + ), + }), +); + +function claudeFileChangeText(output: ClaudeNativeToolOutput): string { + const result = + output.type === "structured_tool_use_result" + ? Option.getOrUndefined(decodeClaudeFileEditResult(output.value)) + : undefined; + if (result === undefined) return claudeNativeToolOutputText(output); + const patch = + result.structuredPatch.length === 0 && result.type === "create" && result.content !== undefined + ? structuredPatch("/dev/null", result.filePath, "", result.content) + : { + oldFileName: result.filePath, + newFileName: result.filePath, + oldHeader: undefined, + newHeader: undefined, + hunks: result.structuredPatch, + }; + return patch.hunks.length === 0 ? "" : formatPatch(patch, FILE_HEADERS_ONLY); +} + function claudeCommandOutputText(output: ClaudeNativeToolOutput): string { const value = claudeNativeToolOutputValue(output); if (typeof value === "object" && value !== null) { @@ -5498,7 +5537,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( const outputText = itemType === "command_execution" ? claudeCommandOutputText(input.output) - : claudeNativeToolOutputText(input.output); + : itemType === "file_change" + ? claudeFileChangeText(input.output) + : claudeNativeToolOutputText(input.output); const turnItem: OrchestrationV2TurnItem = itemType === "command_execution" ? { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2eabadf0a4f9..7f1de3cd97bba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -897,6 +897,9 @@ importers: character-entities: specifier: 2.0.2 version: 2.0.2 + diff: + specifier: 8.0.3 + version: 8.0.3 effect: specifier: 4.0.5 version: 4.0.5 From dac8b11ddc25bd5da62a367fdb6c3de777ae2986 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 28 Oct 2026 22:01:08 -0700 Subject: [PATCH 06/25] chore: vouch nexxeln (#26812) Co-authored-by: Claude Opus 5.5 --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 353a1f41a2616..1c4ae817060a9 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -59,6 +59,7 @@ github:myacoub91 github:naMqe-h github:nateEc github:naveed949 +github:nexxeln github:nmggithub github:Noojuno github:notkainoa From f855f016b3fe103f0b6cb05f26de08cb735b1f7b Mon Sep 17 00:00:00 2001 From: "t3-code-bot[bot]" <269035359+t3-code-bot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:45:11 +0000 Subject: [PATCH 07/25] chore(release): prepare v0.1.4 --- apps/desktop/package.json | 2 +- apps/server/package.json | 2 +- apps/web/package.json | 2 +- packages/contracts/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 728ab36f7fcb2..0d9ab5ae879d3 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.1.3", + "version": "0.1.4", "private": true, "type": "module", "main": "dist-electron/boot.cjs", diff --git a/apps/server/package.json b/apps/server/package.json index 2735d71c6533b..bd81fd993c43e 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "t3", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "repository": { "type": "git", diff --git a/apps/web/package.json b/apps/web/package.json index 729d44f8ea6cd..c35aadeef5d3f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/web", - "version": "0.1.3", + "version": "0.1.4", "private": true, "type": "module", "scripts": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index c87a9fed41033..cff21c52a6e32 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/contracts", - "version": "0.1.3", + "version": "0.1.4", "private": true, "files": [ "dist" From 310c12855238cc191021dfbaaa51aef203374769 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 01:14:00 -0700 Subject: [PATCH 08/25] feat(web): Labs toggle keeps 5h, weekly, and context limits in view (#26859) Co-authored-by: Claude Opus 5.5 --- .../settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/ChatView.tsx | 13 + .../components/chat/ThreadDetailsPanel.tsx | 5 + .../components/chat/ThreadDetailsUsage.tsx | 228 ++++++++++++++++++ .../components/settings/LabsSettingsPanel.tsx | 21 ++ .../src/components/settings/settingsSearch.ts | 6 + docs/user/usage.md | 6 + packages/contracts/src/settings.ts | 2 + 8 files changed, 282 insertions(+) create mode 100644 apps/web/src/components/chat/ThreadDetailsUsage.tsx diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index ea351b6ceb2f4..c80498c14c1fb 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -64,6 +64,7 @@ const clientSettings: ClientSettings = { sidebarThreadSortOrder: "created_at", boringSidebarEnabled: false, boringSidebarMultilineRows: false, + threadDetailsUsageEnabled: false, legacySidebarEnabled: false, sidebarWorkingShelfEnabled: false, loadBalancingEnabled: false, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d6d3e09536070..b9104836b62ee 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1228,6 +1228,9 @@ export default function ChatView(props: ChatViewProps) { const canChangeEnvironment = hasMultipleEnvironments && !envLocked && draftId !== null && !draftThread?.promotedTo; + const threadDetailsUsageEnabled = useClientSettings( + (settings) => settings.threadDetailsUsageEnabled, + ); const loadBalancingSettings = useClientSettings((settings) => ({ loadBalancingEnabled: settings.loadBalancingEnabled, loadBalancingWeights: settings.loadBalancingWeights, @@ -3770,6 +3773,16 @@ export default function ChatView(props: ChatViewProps) { onAddProjectScript={saveProjectScriptStable} onUpdateProjectScript={updateProjectScriptStable} onDeleteProjectScript={deleteProjectScriptStable} + usage={ + threadDetailsUsageEnabled + ? { + providerInstanceId: activeProviderInstanceId, + providers: providerStatuses, + sources: serverConfig?.usageLimitSources ?? EMPTY_USAGE_LIMIT_SOURCES, + contextWindow: activeContextWindow, + } + : undefined + } /> Promise; onDeleteProjectScript: (scriptId: string) => Promise; + /** Set only while the Labs usage readout is on. */ + usage?: ThreadDetailsUsageInput | undefined; } export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { @@ -195,6 +198,8 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { threadId={props.threadId} /> ) : null} + + {props.usage ? : null} )} diff --git a/apps/web/src/components/chat/ThreadDetailsUsage.tsx b/apps/web/src/components/chat/ThreadDetailsUsage.tsx new file mode 100644 index 0000000000000..6a9adadfc7200 --- /dev/null +++ b/apps/web/src/components/chat/ThreadDetailsUsage.tsx @@ -0,0 +1,228 @@ +import type { + ProviderInstanceId, + ServerProvider, + ServerProviderUsageWindow, + UsageLimitSourceSnapshots, +} from "@t3tools/contracts"; +import { + collectProviderUsageLimits, + formatDuration, + type LimitPace, + limitsNotice, + paceOf, + remainingPercent, +} from "@t3tools/shared/usageLimits"; +import type { ReactNode } from "react"; + +import { usePrimarySettings } from "../../hooks/useSettings"; +import { useNowMinute } from "../../hooks/useNowMinute"; +import { type ContextWindowSnapshot, formatContextWindowTokens } from "../../lib/contextWindow"; +import { cn } from "../../lib/utils"; +import { formatUpcomingTimestamp } from "../../timestampFormat"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { ThreadDetailsSection } from "./ThreadDetailsSection"; + +const PACE_LABEL: Record = { + ahead: "Ahead of pace", + on: "On pace", + under: "Under pace", +}; + +export interface ThreadDetailsUsageInput { + readonly providerInstanceId: ProviderInstanceId | null; + readonly providers: readonly ServerProvider[]; + readonly sources: UsageLimitSourceSnapshots; + readonly contextWindow: ContextWindowSnapshot | null; +} + +/** + * Labs readout of the selected provider's limit windows and the thread's context window. + * Every row reads as room left, so a longer bar always means more headroom. + */ +export function ThreadDetailsUsage({ + providerInstanceId, + providers, + sources, + contextWindow, +}: ThreadDetailsUsageInput) { + const nowMinute = useNowMinute(); + const now = Date.parse(`${nowMinute}:00Z`); + const report = + providerInstanceId !== null + ? collectProviderUsageLimits(providerInstanceId, providers, sources, now) + : null; + // Only this instance's data: the report keeps one instance per login, and any other entry may be + // a different login or workspace, so fall back to the instance's own snapshot. + const limits = + report?.accounts.find((entry) => entry.instanceId === providerInstanceId)?.limits ?? + providers.find((provider) => provider.instanceId === providerInstanceId)?.usageLimits; + const notice = limits ? limitsNotice(limits) : null; + if (!limits && !contextWindow) return null; + + return ( + +
+ {notice ? ( + {notice}} + /> + ) : null} + {limits && !notice + ? limits.windows.map((window) => ) + : null} + {contextWindow ? : null} +
+
+ ); +} + +function LimitRow({ + window, + now, +}: { + readonly window: ServerProviderUsageWindow; + readonly now: number; +}) { + const timestampFormat = usePrimarySettings((settings) => settings.timestampFormat); + const left = remainingPercent(window); + const pace = paceOf(window, now); + const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : Number.NaN; + return ( + + + {window.label}: {left}% left + + {window.resetsAt && resetsAt > now ? ( + + Resets in {formatDuration(resetsAt - now)} ·{" "} + {formatUpcomingTimestamp(window.resetsAt, timestampFormat, now)} + + ) : null} + {pace ? {PACE_LABEL[pace]} : null} + + } + /> + ); +} + +function ContextRow({ + usage, + spaced, +}: { + readonly usage: ContextWindowSnapshot; + readonly spaced: boolean; +}) { + const maxTokens = usage.maxTokens ?? null; + const left = + maxTokens !== null && usage.usedPercentage !== null + ? Math.round(100 - Math.max(0, Math.min(100, usage.usedPercentage))) + : null; + const used = formatContextWindowTokens(usage.usedTokens); + return ( + + {left !== null ? Context: {left}% left : null} + + {maxTokens !== null + ? `${used} of ${formatContextWindowTokens(maxTokens)} tokens used` + : `${used} tokens used`} + + + } + value={left === null ? used : undefined} + /> + ); +} + +/** One label, hairline, and value row; the whole row opens the details tooltip. */ +function UsageRow({ + label, + left, + tooltip, + value, + muted = false, + className, +}: { + readonly label: string; + readonly left: number | null; + readonly tooltip: ReactNode; + /** Replaces the percentage when there is none. */ + readonly value?: string | undefined; + readonly muted?: boolean; + readonly className?: string | undefined; +}) { + const tone = left === null ? null : left <= 10 ? "error" : left <= 20 ? "warning" : null; + return ( + + + } + > + {label} + {left !== null ? ( + + {left > 0 ? ( + + ) : null} + + ) : null} + + {value ?? `${left}%`} + + + {/* Rows sit 8px inside the card; clear its edge instead of overlapping it. */} + +
{tooltip}
+
+
+ ); +} diff --git a/apps/web/src/components/settings/LabsSettingsPanel.tsx b/apps/web/src/components/settings/LabsSettingsPanel.tsx index ce3f68eac49d0..1f51e20c0f839 100644 --- a/apps/web/src/components/settings/LabsSettingsPanel.tsx +++ b/apps/web/src/components/settings/LabsSettingsPanel.tsx @@ -18,6 +18,7 @@ export function LabsSettingsPanel() { + {/* Clerk is only mounted when this build has T3 Connect. */} {hasCloudPublicConfig() ? : } @@ -69,6 +70,26 @@ function BoringSidebarMultilineRow() { ); } +function ThreadDetailsUsageRow() { + const settings = useScopedSettings(); + const updateSettings = useUpdateScopedSettings(); + return ( + + updateSettings({ threadDetailsUsageEnabled: Boolean(checked) }) + } + aria-label="Usage in thread details" + /> + } + /> + ); +} + /** Signed out of T3 Connect, the row offers to sign in instead of its switch. */ function GuestLinksRowWithSignIn() { const { isSignedIn } = useAuth(); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 36f63bf64ba8a..0d87e9bd32c83 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -437,6 +437,12 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/labs", searchTerms: ["beta boring sidebar folder rows multiline branch pull request details"], }, + { + id: "thread-details-usage", + title: "Usage in thread details", + to: "/settings/labs", + searchTerms: ["beta limits 5 hour weekly context window tokens quota rate limit"], + }, { id: "guest-links", title: "Guest links", diff --git a/docs/user/usage.md b/docs/user/usage.md index e41e60ca73d76..8064380c5e717 100644 --- a/docs/user/usage.md +++ b/docs/user/usage.md @@ -116,6 +116,12 @@ current model's limits without leaving the conversation. The result opens above closes when you dismiss it or send your next message. It uses the same snapshot as **Usage → Limits**, so it does not run the agent or refresh anything. The command is offered only for providers that appear under **Usage → Limits**. +To keep them in view, turn on **Settings → Labs → Usage in thread details** on web and desktop. +The bottom of the thread details card then shows how much is left in each limit window of the +current provider account and in the thread's context window. Some windows, such as a weekly Opus +limit, count only one model. Hover a row to see when it resets, your pace, or +the token count. + OpenCode Go reports its session, weekly, and monthly allowance when OpenCode runs locally in the environment. T3 cannot report limits for external OpenCode servers because their credentials belong to the remote server. Limits need an OpenCode Go API key, or an OpenCode Console connected diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 438d8fa6ee746..d789770f1616a 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -477,6 +477,7 @@ export const ClientSettingsSchema = Schema.Struct({ boringSidebarMultilineRows: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), ), + threadDetailsUsageEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), // Opt-in for the removed legacy sidebar. Still read so those users land in // the boring sidebar; the boring sidebar toggle clears it. legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -1720,6 +1721,7 @@ export const ClientSettingsPatch = Schema.Struct({ showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), boringSidebarEnabled: Schema.optionalKey(Schema.Boolean), boringSidebarMultilineRows: Schema.optionalKey(Schema.Boolean), + threadDetailsUsageEnabled: Schema.optionalKey(Schema.Boolean), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarWorkingShelfEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), From 6d82a22e9df93f1dd14afa89d820b7dd1b624eef Mon Sep 17 00:00:00 2001 From: tatsuaki watanabe Date: Thu, 29 Oct 2026 17:15:03 +0900 Subject: [PATCH 09/25] fix(desktop): bump Electron to 44.10.0 for the Node.js Brotli fix (CVE-2026-111497) (#26348) Co-authored-by: Claude Opus 5.5 --- apps/desktop/package.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- pnpm-workspace.yaml | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0d9ab5ae879d3..7f768c1b44d75 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -22,7 +22,7 @@ "@t3tools/tailscale": "workspace:*", "dbus-next": "0.10.2", "effect": "catalog:", - "electron": "44.9.3", + "electron": "44.10.0", "electron-store": "^8.2.0", "electron-updater": "^6.8.9", "electron-webauthn": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f1de3cd97bba..087858aeabb82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,7 +416,7 @@ importers: dependencies: '@clerk/electron': specifier: 0.1.3 - version: 0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.9.3(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.10.0(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@clerk/electron-passkeys': specifier: 0.0.3 version: 0.0.3 @@ -451,8 +451,8 @@ importers: specifier: 4.0.5 version: 4.0.5 electron: - specifier: 44.9.3 - version: 44.9.3(supports-color@8.1.1) + specifier: 44.10.0 + version: 44.10.0(supports-color@8.1.1) electron-store: specifier: ^8.2.0 version: 8.2.0 @@ -1011,7 +1011,7 @@ importers: version: 6.40.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@clerk/electron': specifier: 0.1.3 - version: 0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.9.3(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.10.0(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@clerk/react': specifier: 6.18.0 version: 6.18.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -8433,8 +8433,8 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} - electron@44.9.3: - resolution: {integrity: sha512-Cv9/yMnZxNWsMlIe8T4v//NKHdUvbeyoTJoXE3BIcRxus2meeFaO9h/P3zB5d4cBXum51IgAYrT2QjpIDWKpJA==} + electron@44.10.0: + resolution: {integrity: sha512-ss4pCfrQKV4TWkuVYtWB4HmeysPZgB96cSYNKYZv1UVCRP4vQMGNW5oopFbvx6Bbfce1XtP5pm5RDuKehp0svQ==} engines: {node: '>= 22.12.0'} hasBin: true @@ -13213,12 +13213,12 @@ snapshots: '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.9.3(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@clerk/electron@0.1.3(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@44.10.0(supports-color@8.1.1))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: '@clerk/clerk-js': 6.40.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@clerk/react': 6.18.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@clerk/shared': 4.41.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - electron: 44.9.3(supports-color@8.1.1) + electron: 44.10.0(supports-color@8.1.1) react: 19.3.0 tslib: 2.8.1 optionalDependencies: @@ -18059,7 +18059,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron@44.9.3(supports-color@8.1.1): + electron@44.10.0(supports-color@8.1.1): dependencies: '@electron-internal/extract-zip': 1.0.5 '@electron/get': 5.1.0(supports-color@8.1.1) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a07ad2b008d79..acd8c742f836a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -170,7 +170,7 @@ minimumReleaseAgeExclude: - expo-sharing@58.0.19 - expo-sqlite@58.0.13 - expo-symbols@58.0.4 - - electron@44.9.3 + - electron@44.10.0 - "@anthropic-ai/claude-agent-sdk@0.3.276" - "@shadcn/lint@0.1.5" - cn@0.3.2 From da1eab01f712a6c1595ff0f51e7f4fea9b7ebacb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 01:51:16 -0700 Subject: [PATCH 10/25] chore: make agent mistakes fail early (#26167) Co-authored-by: Claude Opus 5.5 --- .github/workflows/agent-context-budget.yml | 2 +- AGENTS.md | 2 +- .../features/threads/PendingUserInputCard.tsx | 4 +- .../features/threads/ThreadDetailScreen.tsx | 2 +- apps/mobile/src/lib/threadActivity.test.ts | 104 ---------- apps/mobile/src/lib/threadActivity.ts | 162 ---------------- .../src/state/use-selected-thread-requests.ts | 2 +- .../src/provider/AgentContextBudget.test.ts | 5 +- apps/web/src/components/chat/ChatComposer.tsx | 2 +- .../chat/ComposerPendingUserInputPanel.tsx | 6 +- .../chat/usePendingThreadRequests.ts | 10 +- apps/web/src/pendingUserInput.test.ts | 156 ++------------- apps/web/src/pendingUserInput.ts | 130 ++----------- docs/operations/development.md | 4 +- packages/client-runtime/package.json | 4 + .../src/state/pendingUserInput.test.ts | 180 ++++++++++++++++++ .../src/state/pendingUserInput.ts | 176 +++++++++++++++++ scripts/check-changed.ts | 103 +++++++--- vite.config.ts | 6 +- 19 files changed, 490 insertions(+), 570 deletions(-) create mode 100644 packages/client-runtime/src/state/pendingUserInput.test.ts create mode 100644 packages/client-runtime/src/state/pendingUserInput.ts diff --git a/.github/workflows/agent-context-budget.yml b/.github/workflows/agent-context-budget.yml index 9396d8e3e7a63..41efb7b69a5a2 100644 --- a/.github/workflows/agent-context-budget.yml +++ b/.github/workflows/agent-context-budget.yml @@ -51,5 +51,5 @@ jobs: echo "Accepted by the agent-context-increase label." exit 0 fi - echo "::error file=$BUDGET::Agent context grew compared to main. Cut the added text, or ask a maintainer to add the agent-context-increase label." + echo "::error file=$BUDGET::Agent context grew compared to main. Cut the added text and rerun the evals that cover it (evals/README.md), or ask a maintainer to add the agent-context-increase label." exit 1 diff --git a/AGENTS.md b/AGENTS.md index cd27babf3f81f..493f7fc0be715 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,7 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real ## Verifying -- Before you push, run `vp run check:changed`. It runs CI's fast checks for changed files: format, lint, knip, typecheck of affected packages and dependents, and tests beside changes. CI still runs the full test suites. +- Before you push, run `vp run check:changed`. It runs CI's fast checks for changed files: format, lint, knip, typecheck of affected packages and dependents, tests beside changes, and the cross-file checks those miss. CI still runs the full test suites. - While you iterate, run `vp test run `. For big files, add `-t ""`. - Use `vp run typecheck`, not `npx tsc`; it caches per package. Both run tsc-rs (`typescript` is the `@tsc-rs/typescript` drop-in). - Run `node -e` snippets from `apps/server` or the owning package. `effect` does not resolve from the repo root. diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index d097416b2ef20..9ecbbb70f1d8e 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -26,9 +26,9 @@ import { useMarkdownPreviewStyles } from "../files/FileMarkdownPreview"; import { useLiveThreadLinkLabels } from "../../state/entities"; import { isPendingUserInputOptionSelected, - type PendingUserInput, type PendingUserInputDraftAnswer, -} from "../../lib/threadActivity"; +} from "@t3tools/client-runtime/state/pending-user-input"; +import type { PendingUserInput } from "../../lib/threadActivity"; export interface PendingUserInputCardProps { readonly environmentId: EnvironmentId; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index a23f06abef24b..27655f503bd65 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -157,10 +157,10 @@ import { appAtomRegistry } from "../../state/atom-registry"; import { threadStreamHaptics } from "./threadStreamHaptics"; import { useAtomCommand } from "../../state/use-atom-command"; import { useDelayedStatus } from "../../lib/useDelayedStatus"; +import type { PendingUserInputDraftAnswer } from "@t3tools/client-runtime/state/pending-user-input"; import type { PendingApproval, PendingUserInput, - PendingUserInputDraftAnswer, ThreadFeedEntry, ThreadFeedLatestRun, } from "../../lib/threadActivity"; diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index 5060f359ebcd3..acdee2249c0d0 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -31,10 +31,6 @@ import { type ThreadFeedCache, type ThreadFeedEntry, type ThreadFeedLatestRun, - togglePendingUserInputOptionSelection, - setPendingUserInputCustomAnswer, - isPendingUserInputOptionSelected, - buildPendingUserInputAnswers, } from "./threadActivity"; const threadId = ThreadId.make("thread-1"); @@ -1368,17 +1364,6 @@ describe("retained v2 feed presentation", () => { }); }); -const singleSelectQuestion = { - id: "runtime", - header: "Runtime", - question: "Which runtime should be used?", - options: [ - { label: "Go", description: "One binary" }, - { label: "Node.js", description: "Reuse TypeScript" }, - ], - multiSelect: false, -} as const; - describe("subagent continued by a later turn", () => { const run2 = RunId.make("run-2"); const at = (second: number) => `2026-10-09T10:00:${String(second).padStart(2, "0")}.000Z`; @@ -1454,95 +1439,6 @@ describe("subagent continued by a later turn", () => { }); }); -describe("pending user input answers", () => { - it("preserves exact editor text, including a deliberately cleared answer", () => { - const question = { ...singleSelectQuestion, initialAnswer: " Proposed message\n" }; - expect( - buildPendingUserInputAnswers([question], { - runtime: { customAnswer: question.initialAnswer }, - }), - ).toEqual({ runtime: question.initialAnswer }); - expect( - buildPendingUserInputAnswers([question], { - runtime: { customAnswer: " Edited message\n\n" }, - }), - ).toEqual({ runtime: " Edited message\n\n" }); - expect(buildPendingUserInputAnswers([question], { runtime: { customAnswer: "" } })).toEqual({ - runtime: "", - }); - expect(buildPendingUserInputAnswers([question], { runtime: { customAnswer: " \n" } })).toEqual({ - runtime: " \n", - }); - expect(setPendingUserInputCustomAnswer(question, { selectedOptionValues: ["Go"] }, "")).toEqual( - { customAnswer: "" }, - ); - }); -}); - -describe("provider question values", () => { - const question = { - ...singleSelectQuestion, - allowCustomAnswer: false, - options: [ - { label: "Same label", value: " exact first ", description: "First" }, - { label: "Same label", value: "second", description: "Second" }, - ], - } as const; - - it("submits raw option values and distinguishes duplicate labels", () => { - const first = togglePendingUserInputOptionSelection(question, undefined, " exact first "); - expect(isPendingUserInputOptionSelected(question, first, " exact first ")).toBe(true); - expect(isPendingUserInputOptionSelected(question, first, "second")).toBe(false); - expect(buildPendingUserInputAnswers([question], { runtime: first })).toEqual({ - runtime: " exact first ", - }); - expect(togglePendingUserInputOptionSelection(question, first, "Same label")).toBe(first); - }); - - it("rejects arbitrary text when the provider only accepts offered options", () => { - expect(setPendingUserInputCustomAnswer(question, undefined, "Other")).toEqual({}); - expect( - buildPendingUserInputAnswers([question], { runtime: { customAnswer: "Other" } }), - ).toBeNull(); - expect( - buildPendingUserInputAnswers([question], { runtime: { selectedOptionValues: ["unknown"] } }), - ).toBeNull(); - expect( - buildPendingUserInputAnswers([question], { - runtime: { selectedOptionValues: ["second"], customAnswer: "stale draft" }, - }), - ).toEqual({ runtime: "second" }); - }); -}); - -it("accepts ready attachment-only answers while preserving selected options", () => { - const question = { - id: "q", - header: "Spec", - question: "Provide a specification", - options: [{ label: "Yes", description: "Approve" }], - multiSelect: false, - }; - expect(buildPendingUserInputAnswers([question], { q: { attachmentCount: 1 } })).toEqual({ - q: "", - }); - expect( - buildPendingUserInputAnswers([question], { - q: { attachmentCount: 1, selectedOptionValues: ["Yes"] }, - }), - ).toEqual({ q: "Yes" }); - expect( - buildPendingUserInputAnswers([question], { - q: { attachmentCount: 1, attachmentsBlocked: true }, - }), - ).toBeNull(); - expect( - buildPendingUserInputAnswers([{ ...question, allowCustomAnswer: false }], { - q: { attachmentCount: 1 }, - }), - ).toBeNull(); -}); - it("stops stranded thinking after a steer and follows the next thought or tool", () => { const at = "2026-06-20T00:00:02.000Z"; const thought = (id: string): OrchestrationV2TurnItem => ({ diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts index fc085a7c50a0d..e17748d77aa6c 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -1,7 +1,6 @@ import type { ThreadPendingApproval, ThreadPendingUserInput, - ThreadUserInputQuestion, } from "@t3tools/client-runtime/state/thread-requests"; import { turnItemIsWorkspacePreparation } from "@t3tools/client-runtime/state/turn-item-presentation"; import type { ThreadCheckpointSummary } from "@t3tools/client-runtime/state/thread-checkpoints"; @@ -81,13 +80,6 @@ import { export type PendingApproval = ThreadPendingApproval; export type PendingUserInput = ThreadPendingUserInput; -export interface PendingUserInputDraftAnswer { - readonly selectedOptionValues?: ReadonlyArray; - readonly customAnswer?: string; - readonly attachmentCount?: number; - readonly attachmentsBlocked?: boolean; -} - export interface ThreadFeedActivity { readonly id: string; readonly createdAt: string; @@ -406,73 +398,6 @@ function isUserInputActivityGroup(entry: ThreadFeedActivityGroup): boolean { return entry.activities.some((activity) => activity.workEntry.questionAnswer !== undefined); } -function normalizeDraftAnswer(value: string | undefined): string | null { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function resolvePendingUserInputOptionValue( - question: ThreadUserInputQuestion, - value: string, -): string | null { - if (question.options.some((option) => option.value === value)) { - return value; - } - - const label = value.trim(); - return label.length > 0 && - question.options.some((option) => option.value === undefined && option.label.trim() === label) - ? label - : null; -} - -function normalizeSelectedOptionValues( - question: ThreadUserInputQuestion, - value: ReadonlyArray | undefined, -): ReadonlyArray { - if (!Array.isArray(value)) { - return []; - } - - return Array.from( - new Set( - value - .map((entry) => resolvePendingUserInputOptionValue(question, entry)) - .filter((entry): entry is string => entry !== null), - ), - ); -} - -function resolvePendingUserInputAnswer( - question: ThreadUserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, -): string | ReadonlyArray | null { - if (draft?.attachmentsBlocked) return null; - const customAnswer = - question.allowCustomAnswer === false - ? null - : question.initialAnswer !== undefined - ? (draft?.customAnswer ?? null) - : normalizeDraftAnswer(draft?.customAnswer); - if (customAnswer !== null && customAnswer.length > 0) { - return customAnswer; - } - - const selectedOptionValues = normalizeSelectedOptionValues(question, draft?.selectedOptionValues); - if (question.multiSelect) { - return selectedOptionValues.length > 0 - ? selectedOptionValues - : (customAnswer ?? - (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null)); - } - return ( - selectedOptionValues[0] ?? - customAnswer ?? - (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null) - ); -} - function capitalizePhrase(value: string): string { const trimmed = value.trim(); return trimmed.length === 0 ? value : `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`; @@ -1974,93 +1899,6 @@ function liveToolActivitySummary(activity: ThreadFeedActivity, presentTense: boo return activity.detail ?? activity.summary; } -export function setPendingUserInputCustomAnswer( - question: ThreadUserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, - customAnswer: string, -): PendingUserInputDraftAnswer { - if (question.allowCustomAnswer === false) { - return draft ?? {}; - } - - const selectedOptionValues = - question.initialAnswer !== undefined || customAnswer.trim().length > 0 - ? undefined - : normalizeSelectedOptionValues(question, draft?.selectedOptionValues); - return { - customAnswer, - ...(selectedOptionValues && selectedOptionValues.length > 0 ? { selectedOptionValues } : {}), - }; -} - -export function isPendingUserInputOptionSelected( - question: ThreadUserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, - optionValue: string, -): boolean { - if (question.allowCustomAnswer !== false && normalizeDraftAnswer(draft?.customAnswer)) { - return false; - } - - const resolvedOptionValue = resolvePendingUserInputOptionValue(question, optionValue); - return ( - resolvedOptionValue !== null && - normalizeSelectedOptionValues(question, draft?.selectedOptionValues).includes( - resolvedOptionValue, - ) - ); -} - -export function togglePendingUserInputOptionSelection( - question: ThreadUserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, - optionValue: string, -): PendingUserInputDraftAnswer { - const resolvedOptionValue = resolvePendingUserInputOptionValue(question, optionValue); - if (resolvedOptionValue === null) { - return draft ?? {}; - } - - if (question.multiSelect) { - const selectedOptionValues = normalizeSelectedOptionValues( - question, - draft?.selectedOptionValues, - ); - const nextSelectedOptionValues = selectedOptionValues.includes(resolvedOptionValue) - ? selectedOptionValues.filter((value) => value !== resolvedOptionValue) - : [...selectedOptionValues, resolvedOptionValue]; - - return { - customAnswer: "", - ...(nextSelectedOptionValues.length > 0 - ? { selectedOptionValues: nextSelectedOptionValues } - : {}), - }; - } - - return { - customAnswer: "", - selectedOptionValues: [resolvedOptionValue], - }; -} - -export function buildPendingUserInputAnswers( - questions: ReadonlyArray, - draftAnswers: Record, -): Record> | null { - const answers: Record> = {}; - - for (const question of questions) { - const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]); - if (answer === null) { - return null; - } - answers[question.id] = answer; - } - - return answers; -} - interface ThreadFeedBuild { readonly rows: ReadonlyArray; readonly attempts: ReadonlyArray | undefined; diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index f869719601750..d4bbc6bfe5bc0 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -42,7 +42,7 @@ import { setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type PendingUserInputDraftAnswer, -} from "../lib/threadActivity"; +} from "@t3tools/client-runtime/state/pending-user-input"; import { appAtomRegistry } from "./atom-registry"; import { useSelectedThreadPendingRequests } from "./use-thread-detail"; import { useThreadSelection } from "./use-thread-selection"; diff --git a/apps/server/src/provider/AgentContextBudget.test.ts b/apps/server/src/provider/AgentContextBudget.test.ts index ad19948c06886..9a26b53bff4e6 100644 --- a/apps/server/src/provider/AgentContextBudget.test.ts +++ b/apps/server/src/provider/AgentContextBudget.test.ts @@ -123,7 +123,10 @@ it.effect("agent context matches the committed budget", () => ), }; - expect(measured).toEqual(budget); + expect( + measured, + "Set agentContextBudget.json to the measured values. Before you cut text to fit, run the evals that cover it (evals/README.md).", + ).toEqual(budget); }).pipe( Effect.provide( McpHttpServer.layerTools.pipe( diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 57838e598161e..d7878cc29a165 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -241,7 +241,7 @@ import { type ThreadShell, } from "../../types"; import { type ComposerPromptHistoryPosition } from "./composerPromptHistory"; -import type { PendingUserInputDraftAnswer } from "../../pendingUserInput"; +import type { PendingUserInputDraftAnswer } from "@t3tools/client-runtime/state/pending-user-input"; import type { LatestProposedPlanState, PendingApproval, diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 697a0e0bfd6f2..e3e14c4a84005 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -3,10 +3,8 @@ import { memo, use, useEffect, useEffectEvent, useRef, useState } from "react"; import { BackgroundThreadContext } from "./backgroundThread"; import { useStableCallback } from "~/hooks/useStableCallback"; import { type PendingUserInput } from "../../session-logic"; -import { - derivePendingUserInputProgress, - type PendingUserInputDraftAnswer, -} from "../../pendingUserInput"; +import type { PendingUserInputDraftAnswer } from "@t3tools/client-runtime/state/pending-user-input"; +import { derivePendingUserInputProgress } from "../../pendingUserInput"; import { CheckboxMark } from "../ui/checkbox"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; import { Kbd } from "../ui/kbd"; diff --git a/apps/web/src/components/chat/usePendingThreadRequests.ts b/apps/web/src/components/chat/usePendingThreadRequests.ts index 35e4cc5f08551..91c5bd0d6c912 100644 --- a/apps/web/src/components/chat/usePendingThreadRequests.ts +++ b/apps/web/src/components/chat/usePendingThreadRequests.ts @@ -27,13 +27,15 @@ import { useStableCallback } from "../../hooks/useStableCallback"; import { getUploadedAttachments, useAttachmentUploadStore } from "../../lib/attachmentUploadQueue"; import { buildPendingUserInputAnswers, + setPendingUserInputCustomAnswer, + togglePendingUserInputOptionSelection, + type PendingUserInputDraftAnswer, +} from "@t3tools/client-runtime/state/pending-user-input"; +import { carryDisplacedCustomAnswerIntoPrompt, derivePendingUserInputProgress, projectPendingUserInputAnswer, - setPendingUserInputCustomAnswer, threadContextsReferencedByAnswer, - togglePendingUserInputOptionSelection, - type PendingUserInputDraftAnswer, } from "../../pendingUserInput"; import { questionAttachmentDraftId, @@ -98,9 +100,9 @@ function makePendingCustomAnswerSetter(input: { [activePendingRequestKey]: { ...existing[activePendingRequestKey], [questionId]: setPendingUserInputCustomAnswer( + question, existing[activePendingRequestKey]?.[questionId], value, - question, ), }, })); diff --git a/apps/web/src/pendingUserInput.test.ts b/apps/web/src/pendingUserInput.test.ts index 69800be4b7574..1178c91043052 100644 --- a/apps/web/src/pendingUserInput.test.ts +++ b/apps/web/src/pendingUserInput.test.ts @@ -5,14 +5,10 @@ import { threadContextReference } from "./lib/composerContextRecords"; import { threadContextRecord } from "@t3tools/shared/composerContextReferences"; import { - buildPendingUserInputAnswers, carryDisplacedCustomAnswerIntoPrompt, derivePendingUserInputProgress, projectPendingUserInputAnswer, - resolvePendingUserInputAnswer, - setPendingUserInputCustomAnswer, threadContextsReferencedByAnswer, - togglePendingUserInputOptionSelection, } from "./pendingUserInput"; describe("projectPendingUserInputAnswer", () => { @@ -68,128 +64,6 @@ const singleSelectQuestion = { multiSelect: false, } as const; -const nativeChoiceQuestion = { - id: "result", - header: "Result", - question: "Which result should be used?", - options: [ - { value: " first\t", label: "Result", description: "First result" }, - { value: "second", label: "Result", description: "Second result" }, - ], - allowCustomAnswer: false, - multiSelect: false, -} as const; - -describe("resolvePendingUserInputAnswer", () => { - it("preserves exact editor text, including a deliberately cleared answer", () => { - const question = { ...singleSelectQuestion, initialAnswer: " Proposed message\n" }; - expect(resolvePendingUserInputAnswer(question, { customAnswer: question.initialAnswer })).toBe( - question.initialAnswer, - ); - expect(resolvePendingUserInputAnswer(question, { customAnswer: " Edited message\n\n" })).toBe( - " Edited message\n\n", - ); - expect(resolvePendingUserInputAnswer(question, { customAnswer: "" })).toBe(""); - expect(resolvePendingUserInputAnswer(question, { customAnswer: " \n" })).toBe(" \n"); - expect( - setPendingUserInputCustomAnswer( - { selectedOptionValues: ["Orchestration-first"] }, - "", - question, - ), - ).toEqual({ customAnswer: "" }); - const drafts = { scope: { customAnswer: "" } }; - expect(derivePendingUserInputProgress([question], drafts, 0)).toMatchObject({ - customAnswer: "", - resolvedAnswer: "", - canAdvance: true, - isComplete: true, - }); - }); - - it("prefers a custom answer over selected options", () => { - expect( - resolvePendingUserInputAnswer(singleSelectQuestion, { - selectedOptionValues: ["Orchestration-first"], - customAnswer: "Keep the existing envelope for one release", - }), - ).toBe("Keep the existing envelope for one release"); - }); - - it("does not replace a required choice with a custom answer", () => { - expect( - resolvePendingUserInputAnswer(nativeChoiceQuestion, { - selectedOptionValues: ["second"], - customAnswer: "Use another result", - }), - ).toBe("second"); - }); - - it("does not submit labels or unknown values when an option has a value", () => { - expect( - resolvePendingUserInputAnswer(nativeChoiceQuestion, { - selectedOptionValues: ["Result", "unknown"], - }), - ).toBeNull(); - }); -}); - -describe("togglePendingUserInputOptionSelection", () => { - it("selects and removes options with the same label independently", () => { - const question = { ...nativeChoiceQuestion, multiSelect: true }; - const firstSelected = togglePendingUserInputOptionSelection(question, undefined, " first\t"); - const bothSelected = togglePendingUserInputOptionSelection(question, firstSelected, "second"); - - expect(buildPendingUserInputAnswers([question], { result: bothSelected })).toEqual({ - result: [" first\t", "second"], - }); - - const secondSelected = togglePendingUserInputOptionSelection( - question, - bothSelected, - " first\t", - ); - expect(buildPendingUserInputAnswers([question], { result: secondSelected })).toEqual({ - result: ["second"], - }); - }); -}); - -describe("buildPendingUserInputAnswers", () => { - it("returns a canonical answer map for complete prompts", () => { - expect( - buildPendingUserInputAnswers( - [ - singleSelectQuestion, - { - id: "compat", - header: "Compat", - question: "How strict should compatibility be?", - options: [ - { - label: "Keep current envelope", - description: "Preserve current wire format", - }, - ], - multiSelect: false, - }, - ], - { - scope: { - selectedOptionValues: ["Orchestration-first"], - }, - compat: { - customAnswer: "Keep the current envelope for one release window", - }, - }, - ), - ).toEqual({ - scope: "Orchestration-first", - compat: "Keep the current envelope for one release window", - }); - }); -}); - describe("pending user input question progress", () => { const questions = [ singleSelectQuestion, @@ -207,6 +81,17 @@ describe("pending user input question progress", () => { }, ] as const; + it("counts a deliberately cleared answer as answered", () => { + const question = { ...singleSelectQuestion, initialAnswer: " Proposed message\n" }; + const drafts = { scope: { customAnswer: "" } }; + expect(derivePendingUserInputProgress([question], drafts, 0)).toMatchObject({ + customAnswer: "", + resolvedAnswer: "", + canAdvance: true, + isComplete: true, + }); + }); + it("derives the active question and advancement state", () => { expect( derivePendingUserInputProgress( @@ -232,25 +117,6 @@ describe("pending user input question progress", () => { }); }); -it("accepts attachment-only answers after every upload finishes", () => { - const questions = [ - { id: "spec", header: "Spec", question: "Provide a spec", options: [], multiSelect: false }, - ]; - expect(buildPendingUserInputAnswers(questions, { spec: { attachmentCount: 1 } })).toEqual({ - spec: "", - }); - expect( - buildPendingUserInputAnswers(questions, { - spec: { attachmentCount: 1, attachmentsBlocked: true }, - }), - ).toBeNull(); - expect( - buildPendingUserInputAnswers([{ ...questions[0]!, allowCustomAnswer: false }], { - spec: { attachmentCount: 1 }, - }), - ).toBeNull(); -}); - describe("carryDisplacedCustomAnswerIntoPrompt", () => { // regression: #12577 clicking a question option discarded typed text it("keeps the thread draft when nothing was typed into the answer", () => { diff --git a/apps/web/src/pendingUserInput.ts b/apps/web/src/pendingUserInput.ts index f8f9d6d52b5ba..17998eaf8010c 100644 --- a/apps/web/src/pendingUserInput.ts +++ b/apps/web/src/pendingUserInput.ts @@ -1,6 +1,13 @@ import type { ThreadContextRecord, UserInputQuestion } from "@t3tools/contracts"; import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; import { formatThreadLink } from "@t3tools/shared/threadLinks"; +import { + buildPendingUserInputAnswers, + normalizeSelectedOptionValues, + resolvePendingUserInputAnswer, + type PendingUserInputAnswer, + type PendingUserInputDraftAnswer, +} from "@t3tools/client-runtime/state/pending-user-input"; import { collectInlineContextIds } from "./lib/composerContextReferences"; @@ -25,20 +32,13 @@ export function threadContextsReferencedByAnswer( return threadContexts.filter((record) => referencedIds.has(record.contextId)); } -export interface PendingUserInputDraftAnswer { - selectedOptionValues?: string[]; - customAnswer?: string; - attachmentCount?: number; - attachmentsBlocked?: boolean; -} - export interface PendingUserInputProgress { questionIndex: number; activeQuestion: UserInputQuestion | null; activeDraft: PendingUserInputDraftAnswer | undefined; - selectedOptionValues: string[]; + selectedOptionValues: ReadonlyArray; customAnswer: string; - resolvedAnswer: string | string[] | null; + resolvedAnswer: PendingUserInputAnswer | null; usingCustomAnswer: boolean; answeredQuestionCount: number; isLastQuestion: boolean; @@ -46,72 +46,6 @@ export interface PendingUserInputProgress { canAdvance: boolean; } -function normalizeDraftAnswer(value: string | undefined): string | null { - if (typeof value !== "string") { - return null; - } - - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeSelectedOptionValues(value: string[] | undefined): string[] { - if (!Array.isArray(value)) { - return []; - } - - // Provider option IDs must stay unchanged, including whitespace. - return Array.from(new Set(value.filter((entry) => typeof entry === "string"))); -} - -export function resolvePendingUserInputAnswer( - question: UserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, -): string | string[] | null { - if (draft?.attachmentsBlocked) return null; - const customAnswer = - question.allowCustomAnswer === false - ? null - : question.initialAnswer !== undefined - ? (draft?.customAnswer ?? null) - : normalizeDraftAnswer(draft?.customAnswer); - if (customAnswer !== null && customAnswer.length > 0) { - return customAnswer; - } - - const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues).filter( - (value) => question.options.some((option) => (option.value ?? option.label) === value), - ); - if (question.multiSelect) { - return selectedOptionValues.length > 0 - ? selectedOptionValues - : (customAnswer ?? - (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null)); - } - - return ( - selectedOptionValues[0] ?? - customAnswer ?? - (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null) - ); -} - -export function setPendingUserInputCustomAnswer( - draft: PendingUserInputDraftAnswer | undefined, - customAnswer: string, - question?: Pick, -): PendingUserInputDraftAnswer { - const selectedOptionValues = - question?.initialAnswer !== undefined || customAnswer.trim().length > 0 - ? undefined - : normalizeSelectedOptionValues(draft?.selectedOptionValues); - - return { - customAnswer, - ...(selectedOptionValues && selectedOptionValues.length > 0 ? { selectedOptionValues } : {}), - }; -} - const DISPLACED_ANSWER_SEPARATOR = "\n\n"; export function carryDisplacedCustomAnswerIntoPrompt( @@ -128,48 +62,6 @@ export function carryDisplacedCustomAnswerIntoPrompt( return `${prompt.trimEnd()}${DISPLACED_ANSWER_SEPARATOR}${displaced}`; } -export function togglePendingUserInputOptionSelection( - question: UserInputQuestion, - draft: PendingUserInputDraftAnswer | undefined, - optionValue: string, -): PendingUserInputDraftAnswer { - if (question.multiSelect) { - const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues); - const nextSelectedOptionValues = selectedOptionValues.includes(optionValue) - ? selectedOptionValues.filter((value) => value !== optionValue) - : [...selectedOptionValues, optionValue]; - - return { - customAnswer: "", - ...(nextSelectedOptionValues.length > 0 - ? { selectedOptionValues: nextSelectedOptionValues } - : {}), - }; - } - - return { - customAnswer: "", - selectedOptionValues: [optionValue], - }; -} - -export function buildPendingUserInputAnswers( - questions: ReadonlyArray, - draftAnswers: Record, -): Record | null { - const answers: Record = {}; - - for (const question of questions) { - const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]); - if (answer === null) { - return null; - } - answers[question.id] = answer; - } - - return answers; -} - function countAnsweredPendingUserInputQuestions( questions: ReadonlyArray, draftAnswers: Record, @@ -203,7 +95,9 @@ export function derivePendingUserInputProgress( questionIndex: normalizedQuestionIndex, activeQuestion, activeDraft, - selectedOptionValues: normalizeSelectedOptionValues(activeDraft?.selectedOptionValues), + selectedOptionValues: activeQuestion + ? normalizeSelectedOptionValues(activeQuestion, activeDraft?.selectedOptionValues) + : [], customAnswer, resolvedAnswer, usingCustomAnswer: diff --git a/docs/operations/development.md b/docs/operations/development.md index 211041c22a24e..3307dbc22b969 100644 --- a/docs/operations/development.md +++ b/docs/operations/development.md @@ -163,7 +163,9 @@ for the security model. `vp run check:changed` runs CI's fast checks for your branch's changes: format and lint of the changed files, `knip:check` when code or a manifest changes, the generated GraphQL checks, typecheck -of the affected packages and their dependents, and the test files beside the changed files. It maps +of the affected packages and their dependents, and the test files beside the changed files. It also +runs the checks that read other files: the agent context budget when the server is affected, and +React Compiler coverage for only the changed web and mobile files. It maps files to packages with [ci-changes.cjs](../../.github/scripts/ci-changes.cjs), as CI does. It compares against the merge base with `origin/main`, or with a ref you pass. CI still runs the full test suites and the jobs the summary lists, so a local pass does not promise a green CI run. diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index 66e79c7492ae3..5808641a5606a 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -331,6 +331,10 @@ "types": "./src/state/threadExecution.ts", "default": "./src/state/threadExecution.ts" }, + "./state/pending-user-input": { + "types": "./src/state/pendingUserInput.ts", + "default": "./src/state/pendingUserInput.ts" + }, "./state/thread-requests": { "types": "./src/state/threadRequests.ts", "default": "./src/state/threadRequests.ts" diff --git a/packages/client-runtime/src/state/pendingUserInput.test.ts b/packages/client-runtime/src/state/pendingUserInput.test.ts new file mode 100644 index 0000000000000..7f5a2b0b84c3d --- /dev/null +++ b/packages/client-runtime/src/state/pendingUserInput.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + buildPendingUserInputAnswers, + isPendingUserInputOptionSelected, + resolvePendingUserInputAnswer, + setPendingUserInputCustomAnswer, + togglePendingUserInputOptionSelection, +} from "./pendingUserInput.ts"; + +const singleSelectQuestion = { + id: "scope", + header: "Scope", + question: "What should the plan target first?", + options: [ + { label: "Orchestration-first", description: "Focus on orchestration first" }, + { label: "Node.js", description: "Reuse TypeScript" }, + ], + multiSelect: false, +} as const; + +const nativeChoiceQuestion = { + id: "result", + header: "Result", + question: "Which result should be used?", + options: [ + { value: " first\t", label: "Result", description: "First result" }, + { value: "second", label: "Result", description: "Second result" }, + ], + allowCustomAnswer: false, + multiSelect: false, +} as const; + +describe("resolvePendingUserInputAnswer", () => { + it("preserves exact editor text, including a deliberately cleared answer", () => { + const question = { ...singleSelectQuestion, initialAnswer: " Proposed message\n" }; + expect(resolvePendingUserInputAnswer(question, { customAnswer: question.initialAnswer })).toBe( + question.initialAnswer, + ); + expect(resolvePendingUserInputAnswer(question, { customAnswer: " Edited message\n\n" })).toBe( + " Edited message\n\n", + ); + expect(resolvePendingUserInputAnswer(question, { customAnswer: "" })).toBe(""); + expect(resolvePendingUserInputAnswer(question, { customAnswer: " \n" })).toBe(" \n"); + expect( + setPendingUserInputCustomAnswer( + question, + { selectedOptionValues: ["Orchestration-first"] }, + "", + ), + ).toEqual({ customAnswer: "" }); + }); + + it("prefers a custom answer over selected options", () => { + expect( + resolvePendingUserInputAnswer(singleSelectQuestion, { + selectedOptionValues: ["Orchestration-first"], + customAnswer: "Keep the existing envelope for one release", + }), + ).toBe("Keep the existing envelope for one release"); + }); + + it("does not replace a required choice with a custom answer", () => { + expect( + resolvePendingUserInputAnswer(nativeChoiceQuestion, { + selectedOptionValues: ["second"], + customAnswer: "Use another result", + }), + ).toBe("second"); + }); + + it("does not submit labels or unknown values when an option has a value", () => { + expect( + resolvePendingUserInputAnswer(nativeChoiceQuestion, { + selectedOptionValues: ["Result", "unknown"], + }), + ).toBeNull(); + }); + + it("matches a label only for an option without a value", () => { + expect( + resolvePendingUserInputAnswer(singleSelectQuestion, { selectedOptionValues: [" Node.js "] }), + ).toBe("Node.js"); + }); + + it("accepts ready attachment-only answers while preserving selected options", () => { + expect(resolvePendingUserInputAnswer(singleSelectQuestion, { attachmentCount: 1 })).toBe(""); + expect( + resolvePendingUserInputAnswer(singleSelectQuestion, { + attachmentCount: 1, + selectedOptionValues: ["Node.js"], + }), + ).toBe("Node.js"); + expect( + resolvePendingUserInputAnswer(singleSelectQuestion, { + attachmentCount: 1, + attachmentsBlocked: true, + }), + ).toBeNull(); + expect( + resolvePendingUserInputAnswer( + { ...singleSelectQuestion, allowCustomAnswer: false }, + { attachmentCount: 1 }, + ), + ).toBeNull(); + }); +}); + +describe("togglePendingUserInputOptionSelection", () => { + it("selects and removes options with the same label independently", () => { + const question = { ...nativeChoiceQuestion, multiSelect: true }; + const firstSelected = togglePendingUserInputOptionSelection(question, undefined, " first\t"); + const bothSelected = togglePendingUserInputOptionSelection(question, firstSelected, "second"); + + expect(buildPendingUserInputAnswers([question], { result: bothSelected })).toEqual({ + result: [" first\t", "second"], + }); + + const secondSelected = togglePendingUserInputOptionSelection( + question, + bothSelected, + " first\t", + ); + expect(buildPendingUserInputAnswers([question], { result: secondSelected })).toEqual({ + result: ["second"], + }); + }); + + it("submits raw option values and leaves the draft alone for an unknown option", () => { + const first = togglePendingUserInputOptionSelection( + nativeChoiceQuestion, + undefined, + " first\t", + ); + expect(isPendingUserInputOptionSelected(nativeChoiceQuestion, first, " first\t")).toBe(true); + expect(isPendingUserInputOptionSelected(nativeChoiceQuestion, first, "second")).toBe(false); + expect(buildPendingUserInputAnswers([nativeChoiceQuestion], { result: first })).toEqual({ + result: " first\t", + }); + expect(togglePendingUserInputOptionSelection(nativeChoiceQuestion, first, "Result")).toBe( + first, + ); + }); +}); + +describe("setPendingUserInputCustomAnswer", () => { + it("rejects arbitrary text when the provider only accepts offered options", () => { + expect(setPendingUserInputCustomAnswer(nativeChoiceQuestion, undefined, "Other")).toEqual({}); + expect( + buildPendingUserInputAnswers([nativeChoiceQuestion], { result: { customAnswer: "Other" } }), + ).toBeNull(); + expect( + buildPendingUserInputAnswers([nativeChoiceQuestion], { + result: { selectedOptionValues: ["second"], customAnswer: "stale draft" }, + }), + ).toEqual({ result: "second" }); + }); +}); + +const compatQuestion = { + id: "compat", + header: "Compat", + question: "How strict should compatibility be?", + options: [{ label: "Keep current envelope", description: "Preserve current wire format" }], + multiSelect: false, +} as const; + +describe("buildPendingUserInputAnswers", () => { + it("returns a canonical answer map for complete prompts", () => { + expect( + buildPendingUserInputAnswers([singleSelectQuestion, compatQuestion], { + scope: { selectedOptionValues: ["Orchestration-first"] }, + compat: { customAnswer: "Keep the current envelope for one release window" }, + }), + ).toEqual({ + scope: "Orchestration-first", + compat: "Keep the current envelope for one release window", + }); + }); +}); diff --git a/packages/client-runtime/src/state/pendingUserInput.ts b/packages/client-runtime/src/state/pendingUserInput.ts new file mode 100644 index 0000000000000..1c71cdb29b4df --- /dev/null +++ b/packages/client-runtime/src/state/pendingUserInput.ts @@ -0,0 +1,176 @@ +/** The question fields answer handling reads. Web and mobile question types both fit it. */ +export interface PendingUserInputQuestion { + readonly id: string; + readonly options: ReadonlyArray<{ readonly label: string; readonly value?: string | undefined }>; + readonly multiSelect?: boolean | undefined; + readonly allowCustomAnswer?: boolean | undefined; + readonly initialAnswer?: string | undefined; +} + +export interface PendingUserInputDraftAnswer { + readonly selectedOptionValues?: ReadonlyArray; + readonly customAnswer?: string; + readonly attachmentCount?: number; + readonly attachmentsBlocked?: boolean; +} + +export type PendingUserInputAnswer = string | ReadonlyArray; + +function normalizeDraftAnswer(value: string | undefined): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +// Provider option values stay exact, including whitespace. Only options without a value match by label. +function resolvePendingUserInputOptionValue( + question: PendingUserInputQuestion, + value: string, +): string | null { + if (question.options.some((option) => option.value === value)) { + return value; + } + + const label = value.trim(); + return label.length > 0 && + question.options.some((option) => option.value === undefined && option.label.trim() === label) + ? label + : null; +} + +/** The draft's selections that name an offered option, deduplicated, in selection order. */ +export function normalizeSelectedOptionValues( + question: PendingUserInputQuestion, + value: ReadonlyArray | undefined, +): ReadonlyArray { + if (!Array.isArray(value)) { + return []; + } + + return Array.from( + new Set( + value + .map((entry) => resolvePendingUserInputOptionValue(question, entry)) + .filter((entry): entry is string => entry !== null), + ), + ); +} + +/** The answer to submit for one question, or null while it is unanswered. */ +export function resolvePendingUserInputAnswer( + question: PendingUserInputQuestion, + draft: PendingUserInputDraftAnswer | undefined, +): PendingUserInputAnswer | null { + if (draft?.attachmentsBlocked) return null; + const customAnswer = + question.allowCustomAnswer === false + ? null + : question.initialAnswer !== undefined + ? (draft?.customAnswer ?? null) + : normalizeDraftAnswer(draft?.customAnswer); + if (customAnswer !== null && customAnswer.length > 0) { + return customAnswer; + } + + const selectedOptionValues = normalizeSelectedOptionValues(question, draft?.selectedOptionValues); + if (question.multiSelect) { + return selectedOptionValues.length > 0 + ? selectedOptionValues + : (customAnswer ?? + (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null)); + } + return ( + selectedOptionValues[0] ?? + customAnswer ?? + (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null) + ); +} + +export function setPendingUserInputCustomAnswer( + question: PendingUserInputQuestion, + draft: PendingUserInputDraftAnswer | undefined, + customAnswer: string, +): PendingUserInputDraftAnswer { + if (question.allowCustomAnswer === false) { + return draft ?? {}; + } + + const selectedOptionValues = + question.initialAnswer !== undefined || customAnswer.trim().length > 0 + ? undefined + : normalizeSelectedOptionValues(question, draft?.selectedOptionValues); + return { + customAnswer, + ...(selectedOptionValues && selectedOptionValues.length > 0 ? { selectedOptionValues } : {}), + }; +} + +export function isPendingUserInputOptionSelected( + question: PendingUserInputQuestion, + draft: PendingUserInputDraftAnswer | undefined, + optionValue: string, +): boolean { + if (question.allowCustomAnswer !== false && normalizeDraftAnswer(draft?.customAnswer)) { + return false; + } + + const resolvedOptionValue = resolvePendingUserInputOptionValue(question, optionValue); + return ( + resolvedOptionValue !== null && + normalizeSelectedOptionValues(question, draft?.selectedOptionValues).includes( + resolvedOptionValue, + ) + ); +} + +/** Toggles an offered option. An unknown option returns the same draft. */ +export function togglePendingUserInputOptionSelection( + question: PendingUserInputQuestion, + draft: PendingUserInputDraftAnswer | undefined, + optionValue: string, +): PendingUserInputDraftAnswer { + const resolvedOptionValue = resolvePendingUserInputOptionValue(question, optionValue); + if (resolvedOptionValue === null) { + return draft ?? {}; + } + + if (question.multiSelect) { + const selectedOptionValues = normalizeSelectedOptionValues( + question, + draft?.selectedOptionValues, + ); + const nextSelectedOptionValues = selectedOptionValues.includes(resolvedOptionValue) + ? selectedOptionValues.filter((value) => value !== resolvedOptionValue) + : [...selectedOptionValues, resolvedOptionValue]; + + return { + customAnswer: "", + ...(nextSelectedOptionValues.length > 0 + ? { selectedOptionValues: nextSelectedOptionValues } + : {}), + }; + } + + return { + customAnswer: "", + selectedOptionValues: [resolvedOptionValue], + }; +} + +/** Every question's answer keyed by question id, or null while any is unanswered. */ +export function buildPendingUserInputAnswers( + questions: ReadonlyArray, + draftAnswers: Record, +): Record | null { + const answers: Record = {}; + + for (const question of questions) { + const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]); + if (answer === null) { + return null; + } + answers[question.id] = answer; + } + + return answers; +} diff --git a/scripts/check-changed.ts b/scripts/check-changed.ts index 25630728e3d66..b854cf0a4e539 100644 --- a/scripts/check-changed.ts +++ b/scripts/check-changed.ts @@ -1,7 +1,9 @@ // @effect-diagnostics nodeBuiltinImport:off globalConsole:off // Runs CI's fast checks for this branch's changes: format and lint of the // changed files, knip, generated GraphQL operations, typecheck of the affected -// packages and their dependents, and the test files beside the changed files. +// packages and their dependents, the test files beside the changed files, and +// the cross-file checks those miss (agent context budget, React Compiler +// coverage of the changed client files). // A dependency change also gets the unused-patch install check below. CI still // runs the full test suites and the jobs listed in the summary. // @@ -138,6 +140,52 @@ for (const file of code) { if (entry.startsWith(prefix) && TEST.test(entry)) tests.add(NodePath.join(dir, entry)); } } +// It reads files other than the ones beside it, so no changed file selects it. +if (affected.includes("t3")) tests.add("apps/server/src/provider/AgentContextBudget.test.ts"); + +// React Compiler coverage compiles every client source file, so run only the +// changed files' cases, in one extra step: a -t filter in the main test step +// would drop every other test. An app is skipped when the main step already +// runs its coverage file unfiltered. File filters mirror each coverage test. +const escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const compilerApps = [ + { + test: "apps/web/src/reactCompilerCoverage.test.ts", + root: "apps/web/src/", + // Test names keep the repo-relative path. + name: (file: string) => file, + compiled: (file: string) => + /\.tsx?$/.test(file) && !/\.(test|browser)\.tsx?$/.test(file) && !file.endsWith(".gen.ts"), + // Web has only per-file cases. + always: [], + }, + { + test: "apps/mobile/src/reactCompilerCoverage.test.ts", + root: "apps/mobile/src/", + name: (file: string) => file.slice("apps/mobile/".length), + compiled: (file: string) => /\.tsx?$/.test(file) && !/\.(test\.tsx?|d\.ts)$/.test(file), + // The Metro, cache, and stale-allowlist cases check more than one file, deletions included. + always: ["^(?!.*compiles )"], + }, +].flatMap(({ test, root: appRoot, name, compiled, always }) => { + const changed = files.filter((file) => file.startsWith(appRoot) && compiled(file)); + if (changed.length === 0 || tests.has(test)) return []; + const names = changed + .filter((file) => present.includes(file)) + .map((file) => `compiles ${escapeRegExp(name(file))}$`); + const pattern = [...names, ...always]; + return pattern.length > 0 ? [{ test, names, pattern }] : []; +}); +const compilerCoverage = compilerApps.length > 0 && { + name: `react compiler (${compilerApps.reduce((count, app) => count + app.names.length, 0)} files)`, + args: [ + "test", + "run", + ...compilerApps.map((app) => app.test), + "-t", + compilerApps.flatMap((app) => app.pattern).join("|"), + ], +}; // knip:check cannot be scoped to a package. Any code change can leave a file, // export, or dependency unused (removing the last import of a helper does), so @@ -255,33 +303,42 @@ const steps = [ name: `test (${tests.size} files)`, args: ["test", "run", "--passWithNoTests", ...tests], }, + compilerCoverage, ].filter((step) => step !== false); // Output is piped, so keep the colors a terminal would get. const env = process.stdout.isTTY ? { ...process.env, FORCE_COLOR: "1" } : process.env; +const runStep = (step: (typeof steps)[number]) => + new Promise<{ name: string; ok: boolean; seconds: string }>((resolve) => { + const started = performance.now(); + const finish = (ok: boolean, output?: Buffer) => { + const seconds = ((performance.now() - started) / 1000).toFixed(1); + console.log(`\n> vp ${step.name} ${ok ? "pass" : "FAIL"} ${seconds}s`); + if (output) process.stdout.write(output); + resolve({ name: step.name, ok, seconds }); + }; + // In-process steps print their own output. The patch check is first and + // takes about a second, so the commands start right after it. + if ("run" in step) return finish(step.run()); + const output: Array = []; + const child = NodeChildProcess.spawn("vp", step.args, { cwd: root, env }); + child.stdout.on("data", (chunk: Buffer) => output.push(chunk)); + child.stderr.on("data", (chunk: Buffer) => output.push(chunk)); + // A failed spawn emits "error", then "close" with a nonzero code. + child.on("error", (error) => output.push(Buffer.from(`${error.message}\n`))); + child.on("close", (status) => finish(status === 0, Buffer.concat(output))); + }); + +// Vitest runs started together race to rebuild node_modules/.vitest-cache after +// an install (ENOTEMPTY), so they run one after another. +let previousVitest: Promise = Promise.resolve(); const results = await Promise.all( - steps.map( - (step) => - new Promise<{ name: string; ok: boolean; seconds: string }>((resolve) => { - const started = performance.now(); - const finish = (ok: boolean, output?: Buffer) => { - const seconds = ((performance.now() - started) / 1000).toFixed(1); - console.log(`\n> vp ${step.name} ${ok ? "pass" : "FAIL"} ${seconds}s`); - if (output) process.stdout.write(output); - resolve({ name: step.name, ok, seconds }); - }; - // In-process steps print their own output. The patch check is first and - // takes about a second, so the commands start right after it. - if ("run" in step) return finish(step.run()); - const output: Array = []; - const child = NodeChildProcess.spawn("vp", step.args, { cwd: root, env }); - child.stdout.on("data", (chunk: Buffer) => output.push(chunk)); - child.stderr.on("data", (chunk: Buffer) => output.push(chunk)); - // A failed spawn emits "error", then "close" with a nonzero code. - child.on("error", (error) => output.push(Buffer.from(`${error.message}\n`))); - child.on("close", (status) => finish(status === 0, Buffer.concat(output))); - }), - ), + steps.map((step) => { + if (!("args" in step) || step.args[0] !== "test") return runStep(step); + const result = previousVitest.then(() => runStep(step)); + previousVitest = result; + return result; + }), ); const summary = ["check:changed summary"]; diff --git a/vite.config.ts b/vite.config.ts index 909becac31779..468837c255686 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -149,7 +149,11 @@ export default defineConfig({ : {}), }, staged: { - "*": "vp fmt --no-error-on-unmatched-pattern", + // In order, so lint reads the formatted file. CI lint failures cost a full round trip. + "*": ["vp fmt --no-error-on-unmatched-pattern", "vp lint --no-error-on-unmatched-pattern"], + // knip checks the whole repo, so it takes no file list. Any code or manifest change can leave + // a file, export, or dependency unused. + "{*.{js,jsx,ts,tsx,mjs,cjs,mts,cts},package.json,knip.jsonc}": () => "vp run knip:check", }, fmt: { ignorePatterns: [ From ccaa3ad2956b90484052198af7f0e40996e403ca Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 02:00:06 -0700 Subject: [PATCH 11/25] feat(marketing): hero shows four agents running at once (#26875) Co-authored-by: Claude Opus 5.5 --- apps/marketing/src/assets/app-desktop.webp | Bin 268254 -> 97242 bytes apps/marketing/src/pages/index.astro | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marketing/src/assets/app-desktop.webp b/apps/marketing/src/assets/app-desktop.webp index 11b51331eef3493fd0abf8e2dc2169c226f1ed0f..48c0c3e9abc5001aea4d04f348d0073938b91eaf 100644 GIT binary patch literal 97242 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 Q_aFcO00000000000Hjpk9RL6T literal 268254 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 z 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 r3l$6=lw*yN$RZ6BYt7G98TVvLXu7J;>OP Date: Thu, 29 Oct 2026 02:17:51 -0700 Subject: [PATCH 12/25] fix(web): undo notice moves to the sidebar footer so it never covers a thread (#26874) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/Sidebar.tsx | 9 +--- .../src/components/sidebar/SidebarChrome.tsx | 8 +--- .../components/sidebar/SidebarListMarkers.tsx | 6 --- .../sidebar/SidebarThreadUndoNotice.tsx | 46 +++++++++++-------- apps/web/src/hooks/showThreadUndoNotice.ts | 2 +- 5 files changed, 31 insertions(+), 40 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c4ba300a030c3..62934576e365b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -3371,7 +3371,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className="mt-auto" label="Working" count={workingThreads.length} - hostsUndoNotice toggle={{ expanded: workingShelfExpanded, onToggle: toggleWorkingShelf, @@ -3387,7 +3386,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve className={cn(workingThreads.length === 0 && "mt-auto")} label="Snoozed" count={snoozedThreads.length} - hostsUndoNotice={workingThreads.length === 0} toggle={{ expanded: snoozedShelfExpanded, onToggle: toggleSnoozedShelf, @@ -3405,9 +3403,6 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve )} label="Settled" count={settledCount} - hostsUndoNotice={ - workingThreads.length + snoozedThreads.length === 0 - } dragging={from !== null} isDropTarget={dragTargetSection === "settled"} toggle={{ @@ -3502,9 +3497,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve ) : null} - + ); } diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index ec161e9998548..e233a4144d435 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -223,19 +223,15 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { /> )} + ); }); -export const SidebarChromeFooter = memo(function SidebarChromeFooter({ - showUndoNotice = false, -}: { - showUndoNotice?: boolean; -}) { +export const SidebarChromeFooter = memo(function SidebarChromeFooter() { return ( - {showUndoNotice ? : null} diff --git a/apps/web/src/components/sidebar/SidebarListMarkers.tsx b/apps/web/src/components/sidebar/SidebarListMarkers.tsx index 3a78876fa5832..104bda5582b4d 100644 --- a/apps/web/src/components/sidebar/SidebarListMarkers.tsx +++ b/apps/web/src/components/sidebar/SidebarListMarkers.tsx @@ -9,7 +9,6 @@ import { type SidebarListMarker, } from "../Sidebar.logic"; import { SIDEBAR_SORTABLE_DRAG_DISABLED } from "./SidebarThreadRow"; -import { SidebarThreadUndoNotice } from "./SidebarThreadUndoNotice"; function SortableSidebarMarker(props: { marker: SidebarListMarker; @@ -117,8 +116,6 @@ export function SidebarSectionHeader(props: { dragging?: boolean; isDropTarget?: boolean; toggle: { expanded: boolean; onToggle: () => void }; - /** The first shelf at the bottom of the list hosts the undo notice just above itself. */ - hostsUndoNotice?: boolean; }) { const shelf = props.marker === "working-header" @@ -132,9 +129,6 @@ export function SidebarSectionHeader(props: { data-testid={`sidebar-${props.marker}`} className={cn("relative mx-0.5 h-8", props.className)} > - {props.hostsUndoNotice ? ( - - ) : null} state.notice); const keybindings = useAtomValue(primaryServerKeybindingsAtom); - const summary = notice ? threadUndoNoticeSummary(notice) : null; + // Short enough to fit beside the utility icons; screen readers hear the full summary. + const summary = notice + ? notice.count === 1 + ? notice.action + : `${notice.action} ${notice.count}` + : null; // Keeps the last text on screen while it fades out, then unmounts on transitionend. const [shownSummary, setShownSummary] = useState(summary); @@ -23,21 +29,23 @@ export function SidebarThreadUndoNotice({ className }: { className?: string }) { if (!text) return null; return ( -

+ // No min-w-0: the undo button sets this item's minimum, so a shared row squeezes its neighbour. + +
{ + if (!summary && event.target === event.currentTarget) setShownSummary(null); + }} + // Reversed wrap: a summary that does not fit beside the undo button drops to a + // second line that overflow hides, instead of truncating mid-word. Browse mode + // reads the button first; the live announcement carries the full sentence. + className={cn( + "flex h-6 min-w-0 flex-1 flex-row-reverse flex-wrap gap-x-1 overflow-hidden text-2xs leading-6 whitespace-nowrap text-sidebar-muted-foreground transition-opacity duration-200 ease-out starting:opacity-0", + !summary && "opacity-0", + )} + > + {threadUndoLabel(keybindings)} + {text}, +
+
); } diff --git a/apps/web/src/hooks/showThreadUndoNotice.ts b/apps/web/src/hooks/showThreadUndoNotice.ts index 5b6b68826fdea..977dcd68d1ab7 100644 --- a/apps/web/src/hooks/showThreadUndoNotice.ts +++ b/apps/web/src/hooks/showThreadUndoNotice.ts @@ -28,7 +28,7 @@ type UndoNotice = { export const useThreadUndoNotice = create<{ notice: UndoNotice | null }>(() => ({ notice: null })); -export function threadUndoNoticeSummary(notice: Pick): string { +function threadUndoNoticeSummary(notice: Pick): string { const noun = notice.action === "Discarded" ? "draft" : "thread"; return `${notice.action} ${notice.count} ${noun}${notice.count === 1 ? "" : "s"}`; } From 58e60bf5e808fc226d9b769d66d1ea9fec014d23 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 02:27:03 -0700 Subject: [PATCH 13/25] feat: turn a No project thread into a project (#26767) Co-authored-by: Claude Opus 5.5 --- .../features/threads/thread-list-v2-items.tsx | 21 +- .../features/threads/turnThreadIntoProject.ts | 47 +++ .../src/environment/ServerEnvironment.ts | 1 + ...OrchestratorMcpToolkit.integration.test.ts | 1 + .../src/observability/RpcInstrumentation.ts | 1 + .../src/orchestration-v2/Orchestrator.ts | 71 +++++ .../src/orchestration-v2/runtimeLayer.test.ts | 276 ++++++++++++++++++ .../src/orchestration-v2/runtimeLayer.ts | 12 + .../src/project/ScratchThreadPromotion.ts | 161 ++++++++++ .../ScheduledTaskService.test.ts | 74 +++++ .../scheduledTasks/ScheduledTaskService.ts | 76 ++++- apps/server/src/ws.test.ts | 3 + apps/server/src/ws.ts | 10 + apps/web/src/components/Sidebar.tsx | 28 +- .../components/threadActionMenu.logic.test.ts | 13 +- .../src/components/threadActionMenu.logic.ts | 13 + apps/web/src/contextMenuFallback.ts | 10 + .../web/src/hooks/useThreadActionMenu.test.ts | 1 + apps/web/src/hooks/useThreadActionMenu.ts | 17 +- apps/web/src/hooks/useThreadActions.ts | 23 ++ docs/user/thread-sidebar.md | 6 + .../src/state/projectCommands.ts | 6 + .../contracts/src/clientRpcPermissions.ts | 1 + packages/contracts/src/environment.ts | 1 + packages/contracts/src/orchestrationV2.ts | 7 + packages/contracts/src/project.ts | 11 + packages/contracts/src/rpc.ts | 10 + 27 files changed, 879 insertions(+), 22 deletions(-) create mode 100644 apps/mobile/src/features/threads/turnThreadIntoProject.ts create mode 100644 apps/server/src/project/ScratchThreadPromotion.ts diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 39c5ac82362a4..5b0c1fd2f5284 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -62,6 +62,8 @@ import { useThreadPr } from "../../state/use-thread-pr"; import { useSwipeRowDormant } from "../home/swipe-row-activation"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { buildThreadTitleRegenerationMenuItems } from "./thread-title-regeneration-menu"; +import { canTurnThreadIntoProject, turnThreadIntoProject } from "./turnThreadIntoProject"; +import { threadCanArchive } from "../home/threadArchive"; import { THREAD_LIST_V2_SETTLED_PAGE_COUNT, resolveThreadListV2SnoozeGateExpiryMs, @@ -837,6 +839,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { : [], [props.autoSettleOptOutSupported, thread.autoSettleDisabledAt], ); + const canTurnIntoProject = canTurnThreadIntoProject(thread); const titleMenuItems = useMemo( () => [ { id: "rename", title: "Rename", image: "square.and.pencil" }, @@ -844,8 +847,23 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { supported: props.titleRegenerationSupported, isRegenerating: thread.titleRegeneration != null, }), + ...(canTurnIntoProject + ? [ + { + id: "turn-into-project", + title: "Turn into project", + image: "folder.badge.plus", + attributes: { disabled: !threadCanArchive(thread.runtime) }, + } satisfies MenuAction, + ] + : []), + ], + [ + canTurnIntoProject, + props.titleRegenerationSupported, + thread.runtime, + thread.titleRegeneration, ], - [props.titleRegenerationSupported, thread.titleRegeneration], ); const snoozableCardMenuActions = useMemo( () => [ @@ -923,6 +941,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { if (nativeEvent.event === "archive") handleArchive(); if (nativeEvent.event === "rename") handleRename(); if (nativeEvent.event === "regenerate-title") handleRegenerateTitle(); + if (nativeEvent.event === "turn-into-project") void turnThreadIntoProject(thread); if (nativeEvent.event === "copy-thread") { const target = threadChipCopyTarget( { environmentId: thread.environmentId, threadId: thread.id }, diff --git a/apps/mobile/src/features/threads/turnThreadIntoProject.ts b/apps/mobile/src/features/threads/turnThreadIntoProject.ts new file mode 100644 index 0000000000000..44e6e03fe0d9b --- /dev/null +++ b/apps/mobile/src/features/threads/turnThreadIntoProject.ts @@ -0,0 +1,47 @@ +import { + isAtomCommandInterrupted, + runAtomCommand, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { AuthOrchestrationOperateScope } from "@t3tools/contracts"; +import * as Haptics from "expo-haptics"; + +import * as Dialog from "../../components/ui/Dialog"; +import { appAtomRegistry } from "../../state/atom-registry"; +import { environmentProjects, projectEnvironment } from "../../state/projects"; +import { environmentServerConfigsAtom } from "../../state/server"; +import { readEnvironmentScope } from "../../state/session"; + +/** A No project thread that already has its own folder, on a server that can promote it. */ +export function canTurnThreadIntoProject(thread: EnvironmentThreadShell): boolean { + if (thread.worktreePath === null) return false; + const config = appAtomRegistry.get(environmentServerConfigsAtom).get(thread.environmentId); + if (config?.environment.capabilities.threadProjectFromScratch !== true) return false; + const project = appAtomRegistry.get( + environmentProjects.projectAtom({ + environmentId: thread.environmentId, + projectId: thread.projectId, + }), + ); + return project?.isScratch === true; +} + +export async function turnThreadIntoProject(thread: EnvironmentThreadShell): Promise { + const title = "Could not turn thread into a project"; + if (!readEnvironmentScope(thread.environmentId, AuthOrchestrationOperateScope)) { + Dialog.alert(title, "This connection cannot change threads."); + return; + } + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + const result = await runAtomCommand( + appAtomRegistry, + projectEnvironment.createFromThread, + { environmentId: thread.environmentId, input: { threadId: thread.id } }, + { reportFailure: false }, + ); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + Dialog.alert(title, error instanceof Error ? error.message : "Try again."); + } +} diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 2b9b1f95b0719..90ae4519bed92 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -246,6 +246,7 @@ export const make = Effect.gen(function* () { threadPinReorder: true, threadActiveReorder: true, threadAutoSettleOptOut: true, + threadProjectFromScratch: true, threadTitleRegeneration: true, commandOutputStreaming: true, threadVisitedTracking: true, diff --git a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts index dba3bf7c08ee4..ec8c9434bec43 100644 --- a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts @@ -182,6 +182,7 @@ const layerUnusedScheduledTaskStub = Layer.succeed( setEnabled: () => Effect.die("ScheduledTaskService.setEnabled is unused in this test"), delete: () => Effect.die("ScheduledTaskService.delete is unused in this test"), deleteForThread: () => Effect.die("unused in this test"), + moveThreadTasks: () => Effect.die("unused in this test"), runNow: () => Effect.die("ScheduledTaskService.runNow is unused in this test"), rotateWebhookToken: () => Effect.die("unused in this test"), listWebhookDeliveries: () => Effect.die("unused in this test"), diff --git a/apps/server/src/observability/RpcInstrumentation.ts b/apps/server/src/observability/RpcInstrumentation.ts index fcbbde88f4b1d..259fa92a66023 100644 --- a/apps/server/src/observability/RpcInstrumentation.ts +++ b/apps/server/src/observability/RpcInstrumentation.ts @@ -150,6 +150,7 @@ const RPC_AGGREGATES = { [WS_METHODS.projectsWriteFile]: "workspace", [WS_METHODS.projectsEnsureScratch]: "orchestration", [WS_METHODS.projectsCreateNew]: "orchestration", + [WS_METHODS.projectsCreateFromThread]: "orchestration", [WS_METHODS.shellOpenInEditor]: "workspace", [WS_METHODS.filesystemBrowse]: "workspace", [WS_METHODS.filesystemGetMetadata]: "workspace", diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 91acb881637c1..d0edd5dacf8f6 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -455,6 +455,23 @@ function wakeWorkStartedAt( const threadPullRequestLinksEqual = Schema.toEquivalence(Schema.NullOr(ThreadLinkedPullRequest)); +/** Unfinished runs (including queued) or open requests block moving a thread to another project. */ +export function threadHasUnfinishedWork(records: { + readonly runs: ReadonlyArray<{ readonly status: string }>; + readonly runtimeRequests: ReadonlyArray<{ readonly status: string }>; +}): boolean { + return ( + records.runs.some( + (run) => + run.status !== "completed" && + run.status !== "failed" && + run.status !== "cancelled" && + run.status !== "interrupted" && + run.status !== "rolled_back", + ) || records.runtimeRequests.some((request) => request.status === "pending") + ); +} + function commandThreadId(command: OrchestrationV2ServerCommand): ThreadId { switch (command.type) { case "thread.create": @@ -486,6 +503,7 @@ function commandThreadId(command: OrchestrationV2ServerCommand): ThreadId { case "thread.pull-request-watch.sync": case "thread.pull-request.sync": case "thread.title.regeneration.complete": + case "thread.project.adopt-folder": case "thread.runtime-mode.set": case "thread.interaction-mode.set": case "thread.model-selection.set": @@ -3983,6 +4001,56 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio }, ); + const dispatchThreadProjectAdoptFolder = Effect.fn( + "orchestrationV2.dispatch.threadProjectAdoptFolder", + )(function* ( + command: Extract< + OrchestrationV2ServerCommand, + { readonly type: "thread.project.adopt-folder" } + >, + events: Ref.Ref>, + ) { + const reject = (cause: string) => + new OrchestratorDispatchError({ + commandId: command.commandId, + commandType: command.type, + cause: `Thread ${command.threadId} ${cause}`, + }); + const projection = yield* projectionStore + .getThreadRecords(command.threadId, ["runs", "runtimeRequests"], { turnItemTypes: [] }) + .pipe( + Effect.mapError( + (cause) => new OrchestratorProjectionError({ threadId: command.threadId, cause }), + ), + ); + const thread = projection.thread; + if (thread.deletedAt !== null) return yield* reject("is deleted."); + if (threadHasUnfinishedWork(projection)) { + return yield* reject("still has work in progress. Finish or stop it first."); + } + const current = yield* projects.get(thread.projectId).pipe(mapDispatchError(command)); + const target = yield* projects.get(command.projectId).pipe(mapDispatchError(command)); + if (Option.isNone(target) || target.value.deletedAt !== null) { + return yield* reject(`cannot move to missing project ${command.projectId}.`); + } + // The thread always lands in the project root, so the move must not change its cwd. + const cwd = thread.worktreePath ?? Option.getOrUndefined(current)?.workspaceRoot; + if (cwd !== target.value.workspaceRoot) { + return yield* reject(`does not work in ${target.value.workspaceRoot}.`); + } + const now = yield* DateTime.now; + yield* emit( + events, + command, + )({ + type: "thread.metadata-updated", + threadId: command.threadId, + providerInstanceId: thread.providerInstanceId, + occurredAt: now, + payload: { ...thread, projectId: command.projectId, worktreePath: null, updatedAt: now }, + }); + }); + const refuseAboveDispatchModeLimit = Effect.fn("orchestrationV2.dispatch.refuseAboveModeLimit")( function* ( command: OrchestrationV2ServerCommand, @@ -12487,6 +12555,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio case "provider-session.detach": yield* dispatchProviderSessionDetach(command, events, effects); break; + case "thread.project.adopt-folder": + yield* dispatchThreadProjectAdoptFolder(command, events); + break; case "message.dispatch": { // Providers own native subagent conversations; messages cannot target them, and adapters ask questions on the parent. const thread = yield* projectionStore diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index c77837094b201..65c0c040e0daf 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -37,6 +37,7 @@ import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; @@ -76,6 +77,7 @@ import { shellStreamItemFromThreadShell } from "./ShellStream.ts"; import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; import * as ThreadManagementService from "./ThreadManagementService.ts"; import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts"; +import * as ScratchThreadPromotion from "../project/ScratchThreadPromotion.ts"; import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts"; import * as Scheduler from "../scheduling/Scheduler.ts"; import * as SecretRequests from "../secrets/SecretRequests.ts"; @@ -5805,6 +5807,198 @@ it.layer(layerTest)("snooze queued runs", (it) => { ); }); +const SCRATCH_ROOT = "/tmp/runtime-layer-promotion-scratch"; +const SCRATCH_PROJECT_ID = ProjectId.make("runtime-layer-promotion-scratch"); +const layerScratchPromotionTest = ScratchThreadPromotion.layer.pipe( + Layer.provide( + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ + scratchRoot: Effect.succeedSome(SCRATCH_ROOT), + isScratchProject: (projectId) => Effect.succeed(projectId === SCRATCH_PROJECT_ID), + namedProjectsRoot: "/projects", + }), + ), + Layer.provide(NodeServices.layer), + Layer.provideMerge(layerProjectDeletionTest), +); + +it.layer(layerScratchPromotionTest)("turning a No project thread into a project", (it) => { + const setup = Effect.fn(function* (name: string) { + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const folder = `${SCRATCH_ROOT}/2026-01-01-${name}`; + if (Option.isNone(yield* projects.getById(SCRATCH_PROJECT_ID))) { + yield* projects.create({ + commandId: CommandId.make("promotion-scratch-create"), + projectId: SCRATCH_PROJECT_ID, + title: "No project", + workspaceRoot: SCRATCH_ROOT, + }); + } + const createThread = (suffix: string, worktreePath: string) => + orchestrator + .dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`promotion-${name}-${suffix}-create`), + threadId: ThreadId.make(`promotion-${name}-${suffix}`), + projectId: SCRATCH_PROJECT_ID, + title: `${name} ${suffix}`, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath, + }) + .pipe(Effect.as(ThreadId.make(`promotion-${name}-${suffix}`))); + return { folder, createThread }; + }); + const shellOf = (threadId: ThreadId) => + Orchestrator.OrchestratorV2.pipe( + Effect.flatMap((orchestrator) => orchestrator.getThreadShell(threadId)), + ); + + it.effect("moves every thread in the folder and its bound tasks into one project", () => + Effect.gen(function* () { + const promotion = yield* ScratchThreadPromotion.ScratchThreadPromotion; + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const tasks = yield* ScheduledTaskService.ScheduledTaskService; + const { folder, createThread } = yield* setup("moves"); + const threadId = yield* createThread("main", folder); + const siblingId = yield* createThread("sibling", folder); + const otherId = yield* createThread("other", `${SCRATCH_ROOT}/2026-01-01-elsewhere`); + yield* orchestrator.dispatch({ + type: "thread.archive", + commandId: CommandId.make("promotion-moves-sibling-archive"), + threadId: siblingId, + }); + const { task } = yield* tasks.upsert({ + id: ScheduledTaskId.make("promotion-moves-bound"), + title: "Bound", + prompt: "Check in", + enabled: true, + schedule: { type: "interval", everyMs: 60_000 }, + projectId: SCRATCH_PROJECT_ID, + threadId: siblingId, + workspaceStrategy: { type: "root" }, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + createdBy: "user", + creationSource: "web", + }); + + const { projectId } = yield* promotion.promote({ threadId }); + + const project = yield* projects.getById(projectId); + assert.equal(Option.getOrUndefined(project)?.workspaceRoot, folder); + assert.equal(Option.getOrUndefined(project)?.title, "moves main"); + for (const id of [threadId, siblingId]) { + const shell = yield* shellOf(id); + assert.equal(shell?.projectId, projectId); + assert.isNull(shell?.worktreePath); + } + assert.equal((yield* shellOf(otherId))?.projectId, SCRATCH_PROJECT_ID); + const saved = (yield* tasks.list()).tasks.find((candidate) => candidate.id === task.id); + assert.equal(saved?.projectId, projectId); + assert.deepEqual(yield* promotion.promote({ threadId }), { projectId }); + }), + ); + + it.effect("changes nothing while a thread in the folder is busy", () => + Effect.gen(function* () { + const promotion = yield* ScratchThreadPromotion.ScratchThreadPromotion; + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const { folder, createThread } = yield* setup("busy"); + const threadId = yield* createThread("idle", folder); + const busyId = yield* createThread("delegate", folder); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("promotion-busy-message"), + threadId: busyId, + messageId: MessageId.make("promotion-busy-message"), + text: "Keep this run active.", + attachments: [], + modelSelection, + dispatchMode: { type: "start_immediately" }, + }); + + const error = yield* promotion.promote({ threadId }).pipe(Effect.flip); + + assert.equal(error.message, '"busy delegate" is still working. Finish or stop it first.'); + assert.isTrue(Option.isNone(yield* projects.getByWorkspaceRoot(folder))); + assert.equal((yield* shellOf(threadId))?.projectId, SCRATCH_PROJECT_ID); + }), + ); + + it.effect("finishes a partial move from either thread", () => + Effect.gen(function* () { + const promotion = yield* ScratchThreadPromotion.ScratchThreadPromotion; + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const { folder, createThread } = yield* setup("partial"); + const movedId = yield* createThread("moved", folder); + const leftId = yield* createThread("left", folder); + const earlier = yield* projects.create({ + commandId: CommandId.make("promotion-partial-project"), + projectId: ProjectId.make("promotion-partial-project"), + title: "Earlier", + workspaceRoot: folder, + }); + yield* orchestrator.dispatch({ + type: "thread.project.adopt-folder", + commandId: CommandId.make("promotion-partial-adopt"), + threadId: movedId, + projectId: earlier.id, + }); + + assert.deepEqual(yield* promotion.promote({ threadId: movedId }), { projectId: earlier.id }); + assert.equal((yield* shellOf(leftId))?.projectId, earlier.id); + }), + ); + + it.effect("refuses a thread outside No project", () => + Effect.gen(function* () { + const promotion = yield* ScratchThreadPromotion.ScratchThreadPromotion; + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const projectId = ProjectId.make("promotion-regular-project"); + const threadId = ThreadId.make("promotion-regular-thread"); + yield* projects.create({ + commandId: CommandId.make("promotion-regular-project"), + projectId, + title: "Regular", + workspaceRoot: "/tmp/runtime-layer-promotion-regular", + }); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("promotion-regular-thread-create"), + threadId, + projectId, + title: "Regular", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + + const error = yield* promotion.promote({ threadId }).pipe(Effect.flip); + + assert.equal( + error.message, + "Only a No project thread with its own folder can become a project.", + ); + }), + ); +}); + it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { it.effect("carries snooze state through the V2 shell projection", () => Effect.gen(function* () { @@ -5900,6 +6094,88 @@ it.layer(layerSharedApplicationDataPlaneTest)("snooze projection", (it) => { assert.isNull(awakened.thread.snoozedAt); }), ); + + it.effect("moves a thread into a project rooted at its own folder", () => + Effect.gen(function* () { + const projects = yield* ProjectService.ProjectService; + const orchestrator = yield* Orchestrator.OrchestratorV2; + const scratchProjectId = ProjectId.make("runtime-layer-adopt-scratch"); + const folderProjectId = ProjectId.make("runtime-layer-adopt-folder"); + const otherProjectId = ProjectId.make("runtime-layer-adopt-other"); + const folder = "/tmp/runtime-layer-adopt-scratch/2026-01-01-notes-a1b2c3d4"; + const createProject = (projectId: ProjectId, workspaceRoot: string) => + projects.create({ + commandId: CommandId.make(`${projectId}-create`), + projectId, + title: projectId, + workspaceRoot, + }); + const createThread = (threadId: ThreadId) => + orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`${threadId}-create`), + threadId, + projectId: scratchProjectId, + title: "Scratch work", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: folder, + }); + const adopt = (threadId: ThreadId, projectId: ProjectId, suffix: string) => + orchestrator.dispatch({ + type: "thread.project.adopt-folder", + commandId: CommandId.make(`${threadId}-adopt-${suffix}`), + threadId, + projectId, + }); + yield* createProject(scratchProjectId, "/tmp/runtime-layer-adopt-scratch"); + yield* createProject(folderProjectId, folder); + yield* createProject(otherProjectId, "/tmp/runtime-layer-adopt-other"); + const idleThreadId = ThreadId.make("runtime-layer-adopt-idle"); + const busyThreadId = ThreadId.make("runtime-layer-adopt-busy"); + yield* createThread(idleThreadId); + yield* createThread(busyThreadId); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-adopt-busy-message"), + threadId: busyThreadId, + messageId: MessageId.make("runtime-layer-adopt-busy-message"), + text: "Keep this run active.", + attachments: [], + modelSelection, + dispatchMode: { type: "start_immediately" }, + }); + + const otherFolder = yield* adopt(idleThreadId, otherProjectId, "other").pipe(Effect.flip); + assert.equal( + otherFolder.cause, + `Thread ${idleThreadId} does not work in /tmp/runtime-layer-adopt-other.`, + ); + const missing = yield* adopt(idleThreadId, ProjectId.make("missing"), "missing").pipe( + Effect.flip, + ); + assert.equal(missing.cause, `Thread ${idleThreadId} cannot move to missing project missing.`); + const busy = yield* adopt(busyThreadId, folderProjectId, "busy").pipe(Effect.flip); + assert.equal( + busy.cause, + `Thread ${busyThreadId} still has work in progress. Finish or stop it first.`, + ); + + yield* adopt(idleThreadId, folderProjectId, "folder"); + const shell = yield* orchestrator.getShellSnapshot(); + const moved = shell.threads.find((thread) => thread.id === idleThreadId); + assert.equal(moved?.projectId, folderProjectId); + assert.isNull(moved?.worktreePath); + const stayed = shell.threads.find((thread) => thread.id === busyThreadId); + assert.equal(stayed?.projectId, scratchProjectId); + }), + ); }); it.layer(layerSharedApplicationDataPlaneTest)("visited projection", (it) => { diff --git a/apps/server/src/orchestration-v2/runtimeLayer.ts b/apps/server/src/orchestration-v2/runtimeLayer.ts index 6bd33ebd26187..1b54627595372 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.ts @@ -16,6 +16,7 @@ import * as AgentSessionScanner from "../project/AgentSessionScanner.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts"; import * as ManagedProjectFolders from "../project/ManagedProjectFolders.ts"; +import * as ScratchThreadPromotion from "../project/ScratchThreadPromotion.ts"; import * as CheckpointCaptureService from "./CheckpointCaptureService.ts"; import * as CheckpointService from "./CheckpointService.ts"; import * as CheckpointRollbackService from "./CheckpointRollbackService.ts"; @@ -289,6 +290,16 @@ const layerScheduledTaskProvided = ScheduledTaskService.layer.pipe( ), ), ); +const layerScratchThreadPromotionProvided = ScratchThreadPromotion.layer.pipe( + Layer.provide( + Layer.mergeAll( + layerProjectService, + layerManagedProjectFoldersProvided, + layerThreadManagementProvided, + layerScheduledTaskProvided, + ), + ), +); const layerProviderContinuationWorkerProvided = ProviderContinuationService.layer.pipe( Layer.provide( Layer.mergeAll( @@ -362,6 +373,7 @@ export const layerProduction = Layer.mergeAll( layerManagedProjectFoldersProvided, layerThreadLaunchProvided, layerScheduledTaskProvided, + layerScratchThreadPromotionProvided, layerSecretRequestsProvided, GuestShareService.layer.pipe( Layer.provide(Layer.merge(layerOrchestratorProvided, layerCommandOutputHubProvided)), diff --git a/apps/server/src/project/ScratchThreadPromotion.ts b/apps/server/src/project/ScratchThreadPromotion.ts new file mode 100644 index 0000000000000..671a480a95832 --- /dev/null +++ b/apps/server/src/project/ScratchThreadPromotion.ts @@ -0,0 +1,161 @@ +import { + CommandId, + type OrchestrationV2ThreadShell, + ProjectId, + type ThreadId, +} from "@t3tools/contracts"; +import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; + +import { threadHasUnfinishedWork } from "../orchestration-v2/Orchestrator.ts"; +import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts"; +import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts"; +import * as ManagedProjectFolders from "./ManagedProjectFolders.ts"; +import * as ProjectService from "./ProjectService.ts"; + +export class ScratchThreadPromotionError extends Schema.TaggedError()( + "ScratchThreadPromotionError", + { + reason: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return this.reason; + } +} + +export class ScratchThreadPromotion extends Context.Service< + ScratchThreadPromotion, + { + /** + * Turns a No project thread's folder into a project, in place, and moves every + * thread working in that folder into it. Running it again finishes a partial move. + */ + readonly promote: (input: { + readonly threadId: ThreadId; + }) => Effect.Effect<{ readonly projectId: ProjectId }, ScratchThreadPromotionError>; + } +>()("t3/project/ScratchThreadPromotion") {} + +const make = Effect.gen(function* () { + const managedFolders = yield* ManagedProjectFolders.ManagedProjectFolders; + const projects = yield* ProjectService.ProjectService; + const threads = yield* ThreadManagementService.ThreadManagementService; + const scheduledTasks = yield* ScheduledTaskService.ScheduledTaskService; + const crypto = yield* Crypto.Crypto; + const path = yield* Path.Path; + + const fail = (reason: string) => (cause: unknown) => + new ScratchThreadPromotionError({ reason, cause }); + + const promote: ScratchThreadPromotion["Service"]["promote"] = Effect.fn( + "ScratchThreadPromotion.promote", + )(function* (input) { + const thread = yield* threads.getThreadShell(input.threadId).pipe( + Effect.mapError(fail("Could not read the thread.")), + Effect.filterOrFail( + (shell) => shell !== null, + () => new ScratchThreadPromotionError({ reason: "Thread not found." }), + ), + ); + const project = yield* projects + .getById(thread.projectId) + .pipe(Effect.mapError(fail("Could not read the thread's project."))); + const folder = thread.worktreePath ?? Option.getOrUndefined(project)?.workspaceRoot; + const scratchRoot = Option.getOrUndefined(yield* managedFolders.scratchRoot); + const isScratch = yield* managedFolders.isScratchProject(thread.projectId); + // A thread already rooted at its folder is a partial earlier promotion; finish it. + const alreadyPromoted = + Option.isSome(project) && project.value.workspaceRoot === folder && !isScratch; + if ( + folder === undefined || + scratchRoot === undefined || + path.dirname(folder) !== scratchRoot || + !(isScratch || alreadyPromoted) + ) { + return yield* new ScratchThreadPromotionError({ + reason: "Only a No project thread with its own folder can become a project.", + }); + } + + const snapshot = yield* Effect.flatten( + threads.readShellSnapshot({ worktreePath: folder }), + ).pipe(Effect.mapError(fail("Could not read the threads in this folder."))); + const moveSet = new Map(); + if (isScratch) moveSet.set(thread.id, thread); + for (const candidate of [...snapshot.threads, ...snapshot.archivedThreads]) { + if (yield* managedFolders.isScratchProject(candidate.projectId)) { + moveSet.set(candidate.id, candidate); + } + } + + for (const candidate of moveSet.values()) { + const records = yield* threads + .getThreadRecords(candidate.id, ["runs", "runtimeRequests"], { turnItemTypes: [] }) + .pipe(Effect.mapError(fail(`Could not read "${candidate.title}".`))); + if (threadHasUnfinishedWork(records)) { + return yield* new ScratchThreadPromotionError({ + reason: `"${candidate.title}" is still working. Finish or stop it first.`, + }); + } + } + + return yield* Effect.uninterruptible( + Effect.gen(function* () { + const projectId = yield* createProject(thread.title, folder); + yield* scheduledTasks + .moveThreadTasks({ threadIds: [...moveSet.keys()], projectId }) + .pipe(Effect.mapError(fail("Could not move the thread's scheduled tasks."))); + for (const candidate of moveSet.values()) { + const commandId = CommandId.make(`scratch-promotion:${yield* randomId}`); + yield* threads + .dispatch({ + type: "thread.project.adopt-folder", + commandId, + threadId: candidate.id, + projectId, + }) + .pipe(Effect.mapError(fail(`Could not move "${candidate.title}". Try again.`))); + } + return { projectId }; + }), + ); + }); + + const randomId = crypto + .randomUUIDv4() + .pipe(Effect.mapError(fail("Could not create an id for the project."))); + + // bootstrap returns the project that already owns the folder, so retries reuse it. + const createProject = Effect.fn("ScratchThreadPromotion.createProject")(function* ( + title: string, + workspaceRoot: string, + ) { + const id = yield* randomId; + return yield* projects + .bootstrap({ + commandId: CommandId.make(`scratch-promotion-project:${id}`), + projectId: ProjectId.make(id), + title, + workspaceRoot, + }) + .pipe( + Effect.map(({ project }) => project.id), + // bootstrap reads before locking; racing creates return the winner in a conflict. + Effect.catchTags({ + ProjectConflictError: (conflict) => Effect.succeed(conflict.conflictingProjectId), + }), + Effect.mapError(fail("Could not create the project.")), + ); + }); + + return ScratchThreadPromotion.of({ promote }); +}); + +export const layer = Layer.effect(ScratchThreadPromotion, make); diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.test.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.test.ts index c1dcdd22abbae..82d165972750f 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.test.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.test.ts @@ -402,3 +402,77 @@ it.effect("thread cleanup notifies subscribers and a finishing run cannot recrea }).pipe(Effect.provide(layer)); }).pipe(Effect.provide(SqlitePersistence.layerMemory)), ); + +it.effect("a bound run follows its thread when promotion moves it mid-send", () => + Effect.gen(function* () { + const scratch = ProjectId.make("project:scratch"); + const promoted = ProjectId.make("project:promoted"); + const sends = yield* Ref.make>([]); + // Rebind lands first, then the promotion commits between the shell read and the retry. + const shellProjects = yield* Ref.make([scratch, promoted]); + const layer = ScheduledTaskService.layer.pipe( + Layer.provide( + Layer.mergeAll( + Layer.mock(ThreadLaunchService.ThreadLaunchService)({}), + Layer.mock(ThreadManagementService.ThreadManagementService)({ + sendToThread: (input) => + Ref.updateAndGet(sends, (all) => [ + ...all, + { projectId: input.projectId, commandId: input.commandId }, + ]).pipe( + Effect.flatMap((all) => + all.length < 3 + ? Effect.fail( + new ThreadManagementService.ThreadManagementThreadNotFoundError({ + projectId: input.projectId, + threadId: input.threadId, + }), + ) + : Effect.succeed({} as never), + ), + ), + getThreadShell: () => + Ref.modify(shellProjects, ([next, ...rest]) => [ + { projectId: next ?? promoted } as never, + rest, + ]), + }), + Layer.mock(SecretRequests.SecretRequests)({}), + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ + namedProjectsRoot: "/projects", + isScratchProject: () => Effect.succeed(false), + }), + NodeCrypto.layer, + Scheduler.layer, + ), + ), + ); + yield* Effect.gen(function* () { + const service = yield* ScheduledTaskService.ScheduledTaskService; + const threadId = ThreadId.make("thread:promoted-during-run"); + const { task } = yield* service.upsert({ + id: ScheduledTaskId.make("task:promoted-during-run"), + title: "Check in", + prompt: "Check progress", + enabled: true, + schedule: { type: "interval", everyMs: 60_000 }, + projectId: promoted, + threadId, + workspaceStrategy: { type: "root" }, + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + }); + + const result = yield* service.runNow({ id: task.id }); + + assert.equal(result.threadId, threadId); + const attempts = yield* Ref.get(sends); + assert.deepEqual( + attempts.map((attempt) => attempt.projectId), + [promoted, scratch, promoted], + ); + assert.equal(new Set(attempts.map((attempt) => attempt.commandId)).size, 1); + }).pipe(Effect.provide(layer)); + }).pipe(Effect.provide(SqlitePersistence.layerMemory)), +); diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.ts index 3dfedb0dfeaf4..b3aa9440944bd 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.ts @@ -2,6 +2,7 @@ import { CommandId, MessageId, PROVIDER_SEND_TURN_MAX_INPUT_CHARS, + type ProjectId, ScheduledTask, ScheduledTaskError, ScheduledTaskId, @@ -240,6 +241,11 @@ export class ScheduledTaskService extends Context.Service< ) => Effect.Effect; /** Deletes only tasks that still target this thread, including their webhook deliveries. */ readonly deleteForThread: (threadId: ThreadId) => Effect.Effect; + /** Points tasks bound to these threads at the project the threads moved to. */ + readonly moveThreadTasks: (input: { + readonly threadIds: ReadonlyArray; + readonly projectId: ProjectId; + }) => Effect.Effect; /** The dispatched thread, or null if dispatch failed; leaves the task's binding unchanged. */ readonly runNow: ( input: ScheduledTaskRunNowInput, @@ -808,6 +814,42 @@ export const layer = Layer.effect( // A webhook prompt was rendered from the row when the request arrived. const prompt = webhook?.prompt ?? active.prompt; + // A bound thread can move to another project (No project to a project) while + // this fires. Follow it; one move needs at most two retries. Same ids replay safely. + const sendBound = ( + threadId: ThreadId, + projectId: ProjectId, + retries: number, + ): ReturnType => + threadManagement + .sendToThread({ + projectId, + commandId, + threadId, + messageId, + scheduledTaskId: active.id, + text: prompt, + attachments: [], + mode: "queue", + createdBy: active.createdBy, + creationSource: active.creationSource, + }) + .pipe( + Effect.catchTags({ + ThreadManagementThreadNotFoundError: (error) => + retries === 0 + ? Effect.fail(error) + : threadManagement.getThreadShell(threadId).pipe( + Effect.orElseSucceed(() => null), + Effect.flatMap((shell) => + shell === null || shell.projectId === projectId + ? Effect.fail(error) + : sendBound(threadId, shell.projectId, retries - 1), + ), + ), + }), + ); + // Capture defects and interrupts too so dispatch failures still reach markCompleted. const result = active.threadId === null @@ -835,20 +877,9 @@ export const layer = Layer.effect( : yield* Effect.exit( // The stored selection only seeds fresh threads. A bound run // follows the thread's current provider, so it passes none. - threadManagement - .sendToThread({ - projectId: active.projectId, - commandId, - threadId: ThreadId.make(active.threadId), - messageId, - scheduledTaskId: active.id, - text: prompt, - attachments: [], - mode: "queue", - createdBy: active.createdBy, - creationSource: active.creationSource, - }) - .pipe(Effect.as(ThreadId.make(active.threadId))), + sendBound(ThreadId.make(active.threadId), active.projectId, 2).pipe( + Effect.as(ThreadId.make(active.threadId)), + ), ); const runSucceeded = result._tag === "Success"; @@ -1262,6 +1293,22 @@ export const layer = Layer.effect( if (removed.length > 0) yield* notifyChanged; }); + const moveThreadTasks: ScheduledTaskService["Service"]["moveThreadTasks"] = Effect.fn( + "ScheduledTaskService.moveThreadTasks", + )(function* (input) { + if (input.threadIds.length === 0) return; + const moved = yield* sql<{ task_id: ScheduledTaskId }>` + UPDATE scheduled_tasks SET project_id = ${input.projectId} + WHERE project_id != ${input.projectId} AND thread_id IN ${sql.in(input.threadIds)} + RETURNING task_id + `.pipe( + Effect.mapError((cause) => + taskError("Could not move the thread's schedule tasks.", { cause }), + ), + ); + if (moved.length > 0) yield* notifyChanged; + }); + const runNow: ScheduledTaskService["Service"]["runNow"] = Effect.fn( "ScheduledTaskService.runNow", )(function* (input: ScheduledTaskRunNowInput) { @@ -2138,6 +2185,7 @@ export const layer = Layer.effect( setEnabled, delete: deleteTask, deleteForThread, + moveThreadTasks, runNow, rotateWebhookToken, listWebhookDeliveries, diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index ac60becc2d78a..bce9450778736 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -49,6 +49,7 @@ import * as EnvironmentTheme from "./environmentTheme.ts"; import * as GitWorkflowService from "./git/GitWorkflowService.ts"; import * as HostResources from "./resourceTelemetry/HostResources.ts"; import * as ManagedProjectFolders from "./project/ManagedProjectFolders.ts"; +import * as ScratchThreadPromotion from "./project/ScratchThreadPromotion.ts"; import * as McpAppRequests from "./mcpApps/McpAppRequests.ts"; import * as ModelManifest from "./provider/ModelManifest.ts"; import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; @@ -593,6 +594,7 @@ it.effect( scratchRoot: Effect.succeedNone, namedProjectsRoot: process.cwd(), }), + Layer.mock(ScratchThreadPromotion.ScratchThreadPromotion)({}), Layer.mock(ServerEnvironment.ServerEnvironment)({ getDescriptor: Effect.succeed({ environmentId: EnvironmentId.make("config-test"), @@ -769,6 +771,7 @@ it.effect("on-demand clients get provider config without workspace snapshots", ( scratchRoot: Effect.succeedNone, namedProjectsRoot: process.cwd(), }), + Layer.mock(ScratchThreadPromotion.ScratchThreadPromotion)({}), Layer.mock(ServerEnvironment.ServerEnvironment)({ getDescriptor: Effect.succeed({ environmentId: EnvironmentId.make("config-test"), diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 34eb11815b13f..8c8509550bc11 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -214,6 +214,7 @@ import * as ProjectEnrichmentService from "./project/ProjectEnrichmentService.ts import * as ProjectService from "./project/ProjectService.ts"; import { threadWorkspaceRoot } from "./project/threadWorkspaceRoot.ts"; import * as ManagedProjectFolders from "./project/ManagedProjectFolders.ts"; +import * as ScratchThreadPromotion from "./project/ScratchThreadPromotion.ts"; import { projectMutationOperation } from "./project/ProjectMutation.ts"; import * as ProjectCloneTracker from "./project/ProjectCloneTracker.ts"; import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts"; @@ -1238,6 +1239,7 @@ export const layerWsRpc = ( const projectStore = yield* ProjectStore.ProjectStoreV2; const projectService = yield* ProjectService.ProjectService; const managedFolders = yield* ManagedProjectFolders.ManagedProjectFolders; + const scratchThreadPromotion = yield* ScratchThreadPromotion.ScratchThreadPromotion; const eventLoopMonitor = yield* EventLoopMonitor.EventLoopMonitor; const threadSearch = yield* ThreadSearch.ThreadSearch; @@ -2716,6 +2718,14 @@ export const layerWsRpc = ( (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }), ), ), + [WS_METHODS.projectsCreateFromThread]: (input) => + scratchThreadPromotion + .promote(input) + .pipe( + Effect.mapError( + (cause) => new OrchestrationDispatchCommandError({ message: cause.message, cause }), + ), + ), [WS_METHODS.projectCloneCancel]: (input) => projectCloneTracker.cancel(input.projectId).pipe(Effect.map((applied) => ({ applied }))), [WS_METHODS.projectCloneRetry]: (input) => diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 62934576e365b..de16be6d2dcfb 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -115,7 +115,11 @@ import { getThreadKeysToDeselectAfterDelete, useThreadSelectionStore, } from "../threadSelectionStore"; -import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadActions"; +import { + canTurnThreadIntoProject, + useAcknowledgeThreadWoke, + useThreadActions, +} from "../hooks/useThreadActions"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useSidebarProjectGroups } from "../hooks/useSidebarProjectGroups"; import { useTerminalFocus } from "../hooks/useTerminalFocus"; @@ -897,6 +901,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve unpinThread, confirmAndUnpinThread, setThreadAutoSettle, + turnThreadIntoProject, reorderPinnedThread, reorderActiveThread, markThreadUnread, @@ -2558,6 +2563,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve canShareWithGuest: appAtomRegistry.get( canShareWithGuestAtom(threadRef.environmentId), ), + canTurnIntoProject: canTurnThreadIntoProject(threadRef), supports: { settlement: configLoaded, autoSettleOptOut: supportsAutoSettleOptOut, @@ -2638,6 +2644,26 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve case "unpin": attemptUnpin(threadRef); return; + case "turn-into-project": { + const result = await turnThreadIntoProject(threadRef); + if (result._tag === "Success") { + toastManager.add({ + type: "success", + title: "Project created", + description: thread.title, + }); + } else if (!isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Could not turn thread into a project", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + return; + } case "auto-settle:enabled": case "auto-settle:disabled": { const result = await setThreadAutoSettle( diff --git a/apps/web/src/components/threadActionMenu.logic.test.ts b/apps/web/src/components/threadActionMenu.logic.test.ts index 039c684dcdc36..bd9dfdcdcc4b6 100644 --- a/apps/web/src/components/threadActionMenu.logic.test.ts +++ b/apps/web/src/components/threadActionMenu.logic.test.ts @@ -65,10 +65,13 @@ describe("buildThreadActionMenuItems", () => { }, ); - it("disables archive while the thread is running", () => { - const archiveItem = buildThreadActionMenuItems({ ...baseState, isRunning: true }).find( - (item) => item.id === "archive", - ); - expect(archiveItem?.disabled).toBe(true); + it("disables archive and turning into a project while the thread is running", () => { + const items = buildThreadActionMenuItems({ + ...baseState, + isRunning: true, + canTurnIntoProject: true, + }); + expect(items.find((item) => item.id === "archive")?.disabled).toBe(true); + expect(items.find((item) => item.id === "turn-into-project")?.disabled).toBe(true); }); }); diff --git a/apps/web/src/components/threadActionMenu.logic.ts b/apps/web/src/components/threadActionMenu.logic.ts index 7779a9db4c3ed..3c4626c1ea1ad 100644 --- a/apps/web/src/components/threadActionMenu.logic.ts +++ b/apps/web/src/components/threadActionMenu.logic.ts @@ -4,6 +4,7 @@ import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled" export type ThreadActionMenuId = | "new-thread-on-branch" | "filter-by-project" + | "turn-into-project" | "project-settings" | "pin" | "unpin" @@ -90,6 +91,8 @@ export interface ThreadActionMenuState { readonly snoozePresets: ReadonlyArray; /** Guest links need their own grant, so the caller checks it instead of `canOperate`. */ readonly canShareWithGuest?: boolean; + /** A No project thread with its own folder, on a server that can promote it. */ + readonly canTurnIntoProject?: boolean; } /** Local navigation, read markers, and copying remain available to read-only clients. */ @@ -183,6 +186,16 @@ export function buildThreadActionMenuItems( }, ] : []), + ...(state.canTurnIntoProject + ? [ + { + id: "turn-into-project" as const, + label: "Turn into project", + icon: "folder-plus", + disabled: state.isRunning, + }, + ] + : []), ...(state.supports.autoSettleOptOut ? [ { diff --git a/apps/web/src/contextMenuFallback.ts b/apps/web/src/contextMenuFallback.ts index 56e85545e6f1e..eece920cba664 100644 --- a/apps/web/src/contextMenuFallback.ts +++ b/apps/web/src/contextMenuFallback.ts @@ -124,6 +124,16 @@ const ICON_PATHS: Record ({ }), })); vi.mock("./useThreadActions", () => ({ + canTurnThreadIntoProject: () => false, useThreadActions: () => Object.fromEntries( [ diff --git a/apps/web/src/hooks/useThreadActionMenu.ts b/apps/web/src/hooks/useThreadActionMenu.ts index b7bec9d5e988d..0e25cebeef878 100644 --- a/apps/web/src/hooks/useThreadActionMenu.ts +++ b/apps/web/src/hooks/useThreadActionMenu.ts @@ -45,7 +45,7 @@ import { threadRuntimeCanArchive } from "@t3tools/client-runtime/state/models"; import { useCopyToClipboard } from "./useCopyToClipboard"; import { useNewThreadHandler } from "./useHandleNewThread"; import { useClientSettings } from "./useSettings"; -import { useThreadActions } from "./useThreadActions"; +import { canTurnThreadIntoProject, useThreadActions } from "./useThreadActions"; function failureToast(title: string, error: unknown) { toastManager.add( @@ -84,6 +84,7 @@ export function useThreadActionMenu(input: { archiveThread, deleteThread, markThreadUnread, + turnThreadIntoProject, } = useThreadActions(); const updateThreadMetadata = useOrchestrationCommand(threadEnvironment.updateMetadata, { reportFailure: false, @@ -144,6 +145,7 @@ export function useThreadActionMenu(input: { supports, snoozePresets, canShareWithGuest, + canTurnIntoProject: canTurnThreadIntoProject(threadRef), }); const clicked = await settlePromise(() => api.contextMenu.show(items, position)); if (clicked._tag === "Failure" || clicked.value === null) return; @@ -254,6 +256,19 @@ export function useThreadActionMenu(input: { case "share-guest": openGuestShareDialog(threadRef); return; + case "turn-into-project": { + const result = await turnThreadIntoProject(threadRef); + if (result._tag === "Success") { + toastManager.add({ + type: "success", + title: "Project created", + description: thread.title, + }); + } else if (!isAtomCommandInterrupted(result)) { + failureToast("Could not turn thread into a project", squashAtomCommandFailure(result)); + } + return; + } case "copy-path": { const workspacePath = thread.worktreePath ?? projectCwd; if (!workspacePath) { diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index 211dc1bef67ef..32020316496ed 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -29,6 +29,7 @@ import { appAtomRegistry } from "../rpc/atomRegistry"; import { environmentServerConfigsAtom } from "../state/server"; import { isScratchProject } from "@t3tools/client-runtime/state/projects"; import { orchestrationEnvironment } from "../state/orchestration"; +import { projectEnvironment } from "../state/projects"; import { threadEnvironment } from "../state/threads"; import { vcsEnvironment } from "../state/vcs"; import { useNewThreadHandler } from "./useHandleNewThread"; @@ -228,6 +229,20 @@ export function useAcknowledgeThreadWoke() { }; } +/** A No project thread that already has its own folder, on a server that can promote it. */ +export function canTurnThreadIntoProject(threadRef: ScopedThreadRef): boolean { + const thread = readThreadShell(threadRef); + const config = appAtomRegistry.get(environmentServerConfigsAtom).get(threadRef.environmentId); + if (!thread?.worktreePath || config?.environment.capabilities.threadProjectFromScratch !== true) { + return false; + } + const project = readProject({ + environmentId: threadRef.environmentId, + projectId: thread.projectId, + }); + return project !== null && isScratchProject(project, config.scratchWorkspaceRoot); +} + function threadOperationFailure(target: ScopedThreadRef) { return readEnvironmentScope(target.environmentId, AuthOrchestrationOperateScope) ? null @@ -279,6 +294,9 @@ export function useThreadActions() { reportFailure: false, }); const markThreadUnread = useMarkThreadUnread(); + const createProjectFromThread = useAtomCommand(projectEnvironment.createFromThread, { + reportFailure: false, + }); const stopThreadSession = useOrchestrationCommand(threadEnvironment.stopSession); const removeWorktree = useAtomCommand(vcsEnvironment.removeWorktree, { reportFailure: false, @@ -925,5 +943,10 @@ export function useThreadActions() { reorderActiveThread, markThreadUnread, setThreadAutoSettle, + turnThreadIntoProject: (target: ScopedThreadRef) => + createProjectFromThread({ + environmentId: target.environmentId, + input: { threadId: target.threadId }, + }), }; } diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index ce16b9df0374b..dfc7b1245ef21 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -37,6 +37,12 @@ folder, so the files the agent wrote stay until you delete them. Branch, worktre these folders are not Git repositories. This is unavailable when the data directory itself sits inside a Git checkout. +When the work grows into something real, pick **Turn into project** from the +thread's menu. The thread's folder becomes a project where it is, so nothing +moves and the agent keeps working in the same place. Other threads in that folder +move with it. Rename the project in its settings, and use **Initialize Git** to +get branch and diff controls. + ### Start in the background In a desktop browser or the desktop app, press `Cmd+Enter` on macOS or `Ctrl+Enter` diff --git a/packages/client-runtime/src/state/projectCommands.ts b/packages/client-runtime/src/state/projectCommands.ts index ede83d82957a8..03db9b869cc06 100644 --- a/packages/client-runtime/src/state/projectCommands.ts +++ b/packages/client-runtime/src/state/projectCommands.ts @@ -201,6 +201,12 @@ export function createProjectEnvironmentAtoms( scheduler: projectScheduler, concurrency: { mode: "serial", key: ({ environmentId }) => environmentId }, }), + createFromThread: createEnvironmentRpcCommand(runtime, { + label: "environment-data:projects:create-from-thread", + tag: WS_METHODS.projectsCreateFromThread, + scheduler: projectScheduler, + concurrency: { mode: "serial", key: ({ environmentId }) => environmentId }, + }), writeFile: createEnvironmentRpcCommand(runtime, { label: "environment-data:projects:write-file", tag: WS_METHODS.projectsWriteFile, diff --git a/packages/contracts/src/clientRpcPermissions.ts b/packages/contracts/src/clientRpcPermissions.ts index 3cd80feea6b73..f9564b2e2441c 100644 --- a/packages/contracts/src/clientRpcPermissions.ts +++ b/packages/contracts/src/clientRpcPermissions.ts @@ -33,6 +33,7 @@ export const CLIENT_GUARDED_RPC_SCOPES = { [WS_METHODS.pullRequestsSetLabels]: AuthSourceControlWriteScope, // Writing a guide runs a model on this environment's providers, which reading one does not. [WS_METHODS.pullRequestsGenerateReviewGuide]: AuthOrchestrationOperateScope, + [WS_METHODS.projectsCreateFromThread]: AuthOrchestrationOperateScope, [WS_METHODS.sourceControlCloneRepository]: AuthSourceControlWriteScope, [WS_METHODS.sourceControlPublishRepository]: AuthSourceControlWriteScope, // Signing in replaces the server's GitHub credential, as saving a token in Settings does. diff --git a/packages/contracts/src/environment.ts b/packages/contracts/src/environment.ts index a65a91e0d3921..fbbffeddc924d 100644 --- a/packages/contracts/src/environment.ts +++ b/packages/contracts/src/environment.ts @@ -110,6 +110,7 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({ threadPinReorder: Schema.optionalKey(Schema.Boolean), threadActiveReorder: Schema.optionalKey(Schema.Boolean), threadAutoSettleOptOut: Schema.optionalKey(Schema.Boolean), + threadProjectFromScratch: Schema.optionalKey(Schema.Boolean), threadTitleRegeneration: Schema.optionalKey(Schema.Boolean), threadVisitedTracking: Schema.optionalKey(Schema.Boolean), threadPullRequestLinking: Schema.optionalKey(Schema.Boolean), diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts index 498f81a0e848a..747d05e7ce4eb 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts @@ -3335,6 +3335,13 @@ const OrchestrationV2InternalCommand = Schema.Union([ text: Schema.String, attachments: Schema.optional(Schema.Array(ChatAttachment)), }), + /** Moves a thread to a project rooted at the folder it already works in, so its cwd never changes. */ + Schema.Struct({ + type: Schema.Literal("thread.project.adopt-folder"), + commandId: CommandId, + threadId: ThreadId, + projectId: ProjectId, + }), Schema.Struct({ type: Schema.Literal("thread.pull-request-watch.sync"), commandId: CommandId, diff --git a/packages/contracts/src/project.ts b/packages/contracts/src/project.ts index 12934a7f8ed70..79e164b71e93b 100644 --- a/packages/contracts/src/project.ts +++ b/packages/contracts/src/project.ts @@ -10,6 +10,7 @@ import { NonNegativeInt, PositiveInt, ProjectId, + ThreadId, TrimmedNonEmptyString, TrimmedString, type UnknownUnionMember, @@ -527,6 +528,16 @@ export const ProjectCreateNewResult = Schema.Struct({ }); export type ProjectCreateNewResult = typeof ProjectCreateNewResult.Type; +export const ProjectCreateFromThreadInput = Schema.Struct({ + threadId: ThreadId, +}); +export type ProjectCreateFromThreadInput = typeof ProjectCreateFromThreadInput.Type; + +export const ProjectCreateFromThreadResult = Schema.Struct({ + projectId: ProjectId, +}); +export type ProjectCreateFromThreadResult = typeof ProjectCreateFromThreadResult.Type; + export class ProjectWriteFileError extends Schema.TaggedError()( "ProjectWriteFileError", { diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 658f6276e247d..aae86bd9db465 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -230,6 +230,8 @@ import { import { ProjectCreateNewInput, ProjectCreateNewResult, + ProjectCreateFromThreadInput, + ProjectCreateFromThreadResult, ProjectEnsureScratchResult, ProjectListEntriesError, ProjectListEntriesInput, @@ -405,6 +407,7 @@ export const WS_METHODS = { projectsMutate: "projects.mutate", projectsEnsureScratch: "projects.ensureScratch", projectsCreateNew: "projects.createNew", + projectsCreateFromThread: "projects.createFromThread", shellOpenInEditor: "shell.openInEditor", @@ -1313,6 +1316,12 @@ const WsProjectsCreateNewRpc = Rpc.make(WS_METHODS.projectsCreateNew, { error: Schema.Union([OrchestrationDispatchCommandError, EnvironmentAuthorizationError]), }); +const WsProjectsCreateFromThreadRpc = Rpc.make(WS_METHODS.projectsCreateFromThread, { + payload: ProjectCreateFromThreadInput, + success: ProjectCreateFromThreadResult, + error: Schema.Union([OrchestrationDispatchCommandError, EnvironmentAuthorizationError]), +}); + const WsShellOpenInEditorRpc = Rpc.make(WS_METHODS.shellOpenInEditor, { payload: LaunchEditorInput, error: Schema.Union([ExternalLauncherError, EnvironmentAuthorizationError]), @@ -2043,6 +2052,7 @@ export const WsRpcGroup = RpcGroup.make( WsProjectsSearchEntriesRpc, WsProjectsEnsureScratchRpc, WsProjectsCreateNewRpc, + WsProjectsCreateFromThreadRpc, WsProjectsWriteFileRpc, WsProjectsMutateRpc, WsShellOpenInEditorRpc, From 4432fbef71817c279290f86549be660af25005f2 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 03:10:52 -0700 Subject: [PATCH 14/25] feat(antigravity): Claude and GPT models, thinking levels, and diff edits (#26876) Co-authored-by: Claude Opus 5.5 --- .../Adapters/AntigravityAdapterV2.test.ts | 80 ++++++------------- .../Adapters/AntigravityAdapterV2.ts | 16 +--- .../src/provider/AntigravityInstallation.ts | 1 - .../src/provider/AntigravityProvider.test.ts | 44 ++++++++++ .../src/provider/AntigravityProvider.ts | 45 ++++++++++- .../src/provider/Drivers/AntigravityDriver.ts | 17 +++- .../acp/AntigravityAcpSupport.test.ts | 36 ++++++++- .../src/provider/acp/AntigravityAcpSupport.ts | 50 ++++++++++-- .../provider/acp/AntigravityClientFiles.ts | 19 ----- .../provider/acp/AntigravityProtocol.test.ts | 7 +- .../src/provider/acp/AntigravityProtocol.ts | 12 ++- .../server/src/provider/antigravityRelease.ts | 63 ++++++++------- apps/server/src/provider/model-manifest.json | 8 +- .../provider/providerCompatibility.test.ts | 18 +++++ .../AntigravityTextGeneration.test.ts | 1 + .../AntigravityTextGeneration.ts | 1 + docs/user/providers-antigravity.md | 12 ++- packages/provider-acp/src/server/adapter.ts | 17 ++-- 18 files changed, 298 insertions(+), 149 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index 4876465a960d3..f69e3696fc45c 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -127,7 +127,7 @@ const layerSession = Layer.mergeAll( ); describe("AntigravityAdapterV2 client file system", () => { - it.effect("confines agent file requests to the workspace under full access", () => + it.effect("serves confined reads and leaves edits to the agent's diff tools", () => Effect.gen(function* () { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const fileSystem = yield* FileSystem.FileSystem; @@ -140,6 +140,8 @@ describe("AntigravityAdapterV2 client file system", () => { // effect-acp keeps the last handler registered per method; so does this. let readTextFile: Parameters[0] | undefined; let writeTextFile: Parameters[0] | undefined; + const logs = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-antigravity-log-" }); + const requestLog = path.join(logs, "requests.ndjson"); const crypto = yield* Crypto.Crypto; const instanceId = ProviderInstanceId.make("antigravity-containment-test"); const adapter = yield* makeAntigravityAdapterV2({ @@ -153,7 +155,7 @@ describe("AntigravityAdapterV2 client file system", () => { command: process.execPath, args: [mockAgentPath], cwd: input.cwd, - env: { T3_ACP_ANTIGRAVITY: "1" }, + env: { T3_ACP_ANTIGRAVITY: "1", T3_ACP_REQUEST_LOG_PATH: requestLog }, }, }).pipe( Effect.provideService(Crypto.Crypto, crypto), @@ -198,17 +200,31 @@ describe("AntigravityAdapterV2 client file system", () => { runtimePolicy, }); yield* session.ensureThread({ threadId, modelSelection, runtimePolicy }); - if (readTextFile === undefined || writeTextFile === undefined) { - return yield* Effect.die("Antigravity sessions must serve client file requests"); + const initialize = (yield* fileSystem.readFileString(requestLog)) + .trim() + .split("\n") + .map( + (line) => + JSON.parse(line) as { + method?: string; + params?: { clientCapabilities?: { fs?: unknown }; clientInfo?: { name?: string } }; + }, + ) + .find((request) => request.method === "initialize"); + assert.deepEqual(initialize?.params?.clientCapabilities?.fs, { + readTextFile: true, + writeTextFile: false, + }); + assert.equal(initialize?.params?.clientInfo?.name, "zed"); + assert.isUndefined(writeTextFile); + if (readTextFile === undefined) { + return yield* Effect.die("Antigravity sessions must serve client file reads"); } const context = (method: string) => ({ requestId: `test-${method}`, method }); const insidePath = path.join(workspace, "src", "inside.ts"); - yield* writeTextFile( - { sessionId: "mock-session-1", path: insidePath, content: "inside" }, - context("fs/write_text_file"), - ); - assert.equal(yield* fileSystem.readFileString(insidePath), "inside"); + yield* fileSystem.makeDirectory(path.dirname(insidePath), { recursive: true }); + yield* fileSystem.writeFileString(insidePath, "inside"); const pasted = yield* readTextFile( { sessionId: "mock-session-1", path: attachment }, context("fs/read_text_file"), @@ -220,13 +236,6 @@ describe("AntigravityAdapterV2 client file system", () => { context("fs/read_text_file"), ).pipe(Effect.exit); assert.isTrue(Exit.isFailure(outsideRead)); - const outsideWrite = yield* writeTextFile( - { sessionId: "mock-session-1", path: path.join(outside, "planted.txt"), content: "x" }, - context("fs/write_text_file"), - ).pipe(Effect.exit); - assert.isTrue(Exit.isFailure(outsideWrite)); - assert.isFalse(yield* fileSystem.exists(path.join(outside, "planted.txt"))); - const linkPath = path.join(workspace, "linked-secret.txt"); yield* fileSystem.symlink(outsideFile, linkPath); const linkedRead = yield* readTextFile( @@ -234,45 +243,6 @@ describe("AntigravityAdapterV2 client file system", () => { context("fs/read_text_file"), ).pipe(Effect.exit); assert.isTrue(Exit.isFailure(linkedRead), "a read through an escaping symlink is denied"); - const linkedWrite = yield* writeTextFile( - { sessionId: "mock-session-1", path: linkPath, content: "overwritten" }, - context("fs/write_text_file"), - ).pipe(Effect.exit); - assert.isTrue(Exit.isFailure(linkedWrite), "a write through an escaping symlink is denied"); - assert.equal(yield* fileSystem.readFileString(outsideFile), "secret"); - const plantedTarget = path.join(outside, "created-through-link.txt"); - const danglingLink = path.join(workspace, "dangling.txt"); - yield* fileSystem.symlink(plantedTarget, danglingLink); - const danglingWrite = yield* writeTextFile( - { sessionId: "mock-session-1", path: danglingLink, content: "planted" }, - context("fs/write_text_file"), - ).pipe(Effect.exit); - assert.isTrue(Exit.isFailure(danglingWrite), "a write through a dangling symlink is denied"); - assert.isFalse(yield* fileSystem.exists(plantedTarget)); - yield* fileSystem.symlink(outside, path.join(workspace, "linked-dir")); - const linkedDirWrite = yield* writeTextFile( - { - sessionId: "mock-session-1", - path: path.join(workspace, "linked-dir", "new.txt"), - content: "planted", - }, - context("fs/write_text_file"), - ).pipe(Effect.exit); - assert.isTrue(Exit.isFailure(linkedDirWrite), "a write under an escaping directory link"); - assert.isFalse(yield* fileSystem.exists(path.join(outside, "new.txt"))); - const nestedLinkedDirWrite = yield* writeTextFile( - { - sessionId: "mock-session-1", - path: path.join(workspace, "linked-dir", "a", "b", "file.txt"), - content: "planted", - }, - context("fs/write_text_file"), - ).pipe(Effect.exit); - assert.isTrue( - Exit.isFailure(nestedLinkedDirWrite), - "a write to missing directories under an escaping directory link", - ); - assert.isFalse(yield* fileSystem.exists(path.join(outside, "a"))); const insideLink = path.join(workspace, "inside-link.ts"); yield* fileSystem.symlink(insidePath, insideLink); const viaInsideLink = yield* readTextFile( diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts index 2c41e007edf65..36c58a29ecaee 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts @@ -23,10 +23,7 @@ import { buildAntigravityPrompt, type AntigravityAcpRuntimeInput, } from "../../provider/acp/AntigravityAcpSupport.ts"; -import { - readAntigravityClientTextFile, - writeAntigravityClientTextFile, -} from "../../provider/acp/AntigravityClientFiles.ts"; +import { readAntigravityClientTextFile } from "../../provider/acp/AntigravityClientFiles.ts"; import { antigravityApprovalOptions, antigravitySubagentOutput, @@ -82,7 +79,7 @@ export interface AntigravityAdapterV2Options { readonly continuationRequests?: Parameters[0]["continuationRequests"]; } -/** ACP 1.1.1 reports `start_subagent` tools without child session ids. */ +/** Native subagent tools report no child session ids. */ const extractAntigravitySubagentUpdate: NonNullable = ( toolCall, ) => { @@ -126,7 +123,7 @@ export function makeAntigravityAcpAdapterFlavor( Scope.close(scope, Exit.void), options.makeRuntime({ ...input, - clientFileSystem: true, + clientFileReads: true, additionalDirectories: [options.host.paths.attachmentsDir], }), ) @@ -191,13 +188,6 @@ export function makeAntigravityAcpAdapterFlavor( allowedRoots: antigravityClientFileRoots(cwd), request, }), - writeTextFile: (request, cwd) => - writeAntigravityClientTextFile({ - fileSystem: options.fileSystem, - path: options.path, - allowedRoots: antigravityClientFileRoots(cwd), - request, - }), }, approvalOptions: antigravityApprovalOptions, extractPermissionQuestion: (request) => { diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 3d590abda0bdb..902e6508ca25d 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -398,7 +398,6 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma }), ) : spawner, - clientInfo: { name: "t3-code", version: "0.0.0" }, }); const initialized = yield* runtime.initialize(); if ( diff --git a/apps/server/src/provider/AntigravityProvider.test.ts b/apps/server/src/provider/AntigravityProvider.test.ts index ade033551f655..35f279f29ddcc 100644 --- a/apps/server/src/provider/AntigravityProvider.test.ts +++ b/apps/server/src/provider/AntigravityProvider.test.ts @@ -214,6 +214,50 @@ describe("Antigravity model catalog", () => { "gemini-pro-agent", ); }); + + it("offers each model the thinking levels it reported when selected", () => { + const thinking = (currentValue: string) => + ({ + id: "thinking_level", + name: "Thinking Level", + category: "thought_level", + type: "select", + currentValue, + options: [ + { value: "low", name: "Low" }, + { value: "high", name: "High" }, + ], + }) satisfies EffectAcpSchema.SessionConfigOption; + const models = buildAntigravityModelsFromSession( + { + configOptions: [ + { + ...modelConfig, + currentValue: "gemini", + options: [ + { value: "gemini", name: "Gemini" }, + { value: "claude", name: "Claude" }, + { value: "plain", name: "Plain" }, + { value: "unseen", name: "Unseen" }, + ], + }, + thinking("high"), + ], + }, + new Map([ + ["claude", thinking("low")], + ["plain", null], + ]), + ); + const thinkingOf = (slug: string) => + models.find((model) => model.slug === slug)?.capabilities?.optionDescriptors?.[0]; + + expect(thinkingOf("gemini")).toMatchObject({ id: "thinking_level", currentValue: "high" }); + expect(thinkingOf("claude")).toMatchObject({ id: "thinking_level", currentValue: "low" }); + expect(thinkingOf("plain")).toBeUndefined(); + expect(thinkingOf("unseen")).toMatchObject({ id: "thinking_level" }); + expect(thinkingOf("unseen")).not.toHaveProperty("currentValue"); + }); }); it.layer(layerTest)("Antigravity provider snapshots", (it) => { diff --git a/apps/server/src/provider/AntigravityProvider.ts b/apps/server/src/provider/AntigravityProvider.ts index c6809c3577132..988661a7ce614 100644 --- a/apps/server/src/provider/AntigravityProvider.ts +++ b/apps/server/src/provider/AntigravityProvider.ts @@ -20,6 +20,7 @@ import type * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpSchema from "effect-acp/compat"; import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; +import { acpProviderOptionDescriptors } from "@t3tools/provider-acp/server/sessionConfig"; import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; import { makeManualOnlyProviderMaintenanceCapabilities, @@ -32,6 +33,8 @@ import { } from "@t3tools/provider-core/server/snapshotProbe"; import { resolveUsageLimitsAfterProbe } from "@t3tools/provider-core/server/usageLimits"; +import { antigravityThinkingConfig } from "./acp/AntigravityAcpSupport.ts"; + const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] }); const MAX_WORKSPACE_SNAPSHOTS = 32; const HEALTH_CHECK_TIMEOUT = "90 seconds"; @@ -44,8 +47,24 @@ type SessionSetupResult = Pick< "configOptions" | "models" >; +type ThinkingConfig = NonNullable>; + +/** Thinking options each model reported when selected; `null` means it has none. */ +export type AntigravityThinkingLevels = ReadonlyMap; + +function thinkingCapabilities(config: ThinkingConfig | null | undefined, observed: boolean) { + const descriptor = config + ? acpProviderOptionDescriptors({ configOptions: [config], modeState: undefined })[0] + : undefined; + if (descriptor?.type !== "select") return EMPTY_MODEL_CAPABILITIES; + // Another model's selected level is not this model's default. + const { currentValue: _currentValue, ...unobserved } = descriptor; + return createModelCapabilities({ optionDescriptors: [observed ? descriptor : unobserved] }); +} + export function buildAntigravityModelsFromSession( setup: SessionSetupResult, + thinkingLevels: AntigravityThinkingLevels = new Map(), ): ReadonlyArray { const config = setup.configOptions?.find( (option) => option.id === "model" || option.category === "model", @@ -61,10 +80,13 @@ export function buildAntigravityModelsFromSession( name: model.name, })) ?? []) : []; + const currentThinking = antigravityThinkingConfig(setup.configOptions); const seen = new Set(); return entries.flatMap((entry): ServerProviderModel[] => { if (!entry.value.trim() || seen.has(entry.value)) return []; seen.add(entry.value); + const observed = + entry.value === currentValue ? (currentThinking ?? null) : thinkingLevels.get(entry.value); return [ { slug: entry.value, @@ -73,7 +95,10 @@ export function buildAntigravityModelsFromSession( ...(entry.value === currentValue ? { isDefault: true, aliases: [ANTIGRAVITY_DEFAULT_MODEL] } : {}), - capabilities: EMPTY_MODEL_CAPABILITIES, + capabilities: + observed === undefined + ? thinkingCapabilities(currentThinking, false) + : thinkingCapabilities(observed, true), }, ]; }); @@ -162,6 +187,17 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func }); // Preserve disk-discovered skills when callbacks replace workspace commands; the registry will not rescan them. const discoveredSkills = new Map(); + const thinkingLevels = new Map(); + const observeThinkingLevels = ( + configOptions: ReadonlyArray | null | undefined, + ) => { + const model = configOptions?.find( + (option) => option.id === "model" || option.category === "model", + ); + if (model?.type === "select" && model.currentValue) { + thinkingLevels.set(model.currentValue, antigravityThinkingConfig(configOptions) ?? null); + } + }; const getSnapshot = SubscriptionRef.get(metadata).pipe( Effect.flatMap((state) => options.stampIdentity(state.draft)), ); @@ -267,6 +303,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func cwd?: string, ) { if (!settings.enabled) return; + observeThinkingLevels(started.sessionSetupResult.configOptions); const before = yield* SubscriptionRef.get(metadata); const supportsTextGeneration = yield* options.supportsTextGeneration; const usageLimits = @@ -301,7 +338,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func published: draft.usageLimits, probed: usageLimits ?? draft.usageLimits, }), - models: buildAntigravityModelsFromSession(started.sessionSetupResult), + models: buildAntigravityModelsFromSession(started.sessionSetupResult, thinkingLevels), supportsTextGeneration, ...(cwd ? { @@ -324,7 +361,8 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func const onConfigOptionsUpdated = Effect.fn("AntigravityProvider.onConfigOptionsUpdated")(function* ( configOptions: ReadonlyArray, ) { - const models = buildAntigravityModelsFromSession({ configOptions }); + observeThinkingLevels(configOptions); + const models = buildAntigravityModelsFromSession({ configOptions }, thinkingLevels); yield* SubscriptionRef.update(metadata, (state) => { if (state.draft.auth.status !== "authenticated") return state; return { ...state, draft: { ...state.draft, models } }; @@ -391,6 +429,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func }) satisfies AntigravityProviderState, ); discoveredSkills.clear(); + thinkingLevels.clear(); }); const snapshotForCwd = Effect.fn("AntigravityProvider.snapshotForCwd")(function* ( diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index fd5725c6cd798..f1f39e104b44c 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -38,6 +38,8 @@ import { type AntigravityAuthConfig, } from "../antigravityAuthSupport.ts"; import { + antigravityModelOptions, + antigravityThinkingConfig, makeAntigravityAcpRuntime, type AntigravityAcpRuntimeInput, } from "../acp/AntigravityAcpSupport.ts"; @@ -294,7 +296,6 @@ export const AntigravityDriver: ProviderDriver< ); const runtime = yield* makeRuntime({ cwd, - clientInfo: { name: "t3-code-provider-setup", version: "0.0.0" }, mcpServers: [], ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), }); @@ -313,7 +314,10 @@ export const AntigravityDriver: ProviderDriver< const publishCatalog = Effect.fn("AntigravityDriver.publishCatalog")(function* ( started: AcpSessionRuntime.AcpSessionRuntimeStartResult, - runtime: Pick, + runtime: Pick< + AcpSessionRuntime.AcpSessionRuntime["Service"], + "getEvents" | "drainEvents" | "setModel" + >, ) { yield* provider.onSessionStarted(started); yield* Stream.runForEach(runtime.getEvents(), (event) => { @@ -327,6 +331,14 @@ export const AntigravityDriver: ProviderDriver< ? provider.onAvailableCommands(event.availableCommands) : Effect.void; }).pipe(Effect.forkScoped); + // Thinking levels differ per model and are reported only for the selected one. + const configOptions = started.sessionSetupResult.configOptions ?? []; + const current = configOptions.find((option) => option.id === "model")?.currentValue; + if (antigravityThinkingConfig(configOptions) !== undefined && typeof current === "string") { + for (const model of [...antigravityModelOptions(configOptions), { value: current }]) { + yield* runtime.setModel(model.value).pipe(Effect.ignore); + } + } yield* runtime.drainEvents; }, Effect.scoped); @@ -439,7 +451,6 @@ export const AntigravityDriver: ProviderDriver< makeRuntime: (cwd) => makeRuntime({ cwd, - clientInfo: { name: "t3-code-text", version: "0.0.0" }, mcpServers: [], }), }); diff --git a/apps/server/src/provider/acp/AntigravityAcpSupport.test.ts b/apps/server/src/provider/acp/AntigravityAcpSupport.test.ts index bd48ee767ee46..8e0eed1371c5b 100644 --- a/apps/server/src/provider/acp/AntigravityAcpSupport.test.ts +++ b/apps/server/src/provider/acp/AntigravityAcpSupport.test.ts @@ -38,8 +38,18 @@ function makeModelRuntime( if (failure) return yield* failure; selections.push(model); }); + const setConfigOption = Effect.fn("AntigravityAcpSupportTest.setConfigOption")(function* ( + configId: string, + value: string | boolean, + options?: { readonly allowUnlistedValue?: boolean }, + ) { + selections.push( + `${configId}=${String(value)}${options?.allowUnlistedValue === true ? " (unlisted)" : ""}`, + ); + return { configOptions }; + }); return { - runtime: { getConfigOptions: Effect.succeed(configOptions), setModel }, + runtime: { getConfigOptions: Effect.succeed(configOptions), setModel, setConfigOption }, selections, }; } @@ -137,6 +147,30 @@ describe("applyAntigravityAcpModelSelection", () => { expect(selections).toEqual(["gemini-saved"]); }), ); + + it.effect("passes a model ID saved on runtime 1.3 to the agent as its 1.4 family", () => + Effect.gen(function* () { + // The cold-resume model has no thinking option, so the level is unknown locally. + const { runtime, selections } = makeModelRuntime([ + { + ...modelConfig, + currentValue: "gpt-oss-120b-medium", + options: [ + { value: "gemini-3.8-flash", name: "Gemini 3.8 Flash" }, + { value: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)" }, + ], + }, + ]); + const model = yield* applyAntigravityAcpModelSelection({ + runtime, + model: "gemini-3.8-flash-low", + mapError: (cause) => cause, + }); + + expect(model).toBe("gemini-3.8-flash"); + expect(selections).toEqual(["model=gemini-3.8-flash-low (unlisted)"]); + }), + ); }); const imageAttachment = { diff --git a/apps/server/src/provider/acp/AntigravityAcpSupport.ts b/apps/server/src/provider/acp/AntigravityAcpSupport.ts index ac64c9da2a469..a57fc08f9b7af 100644 --- a/apps/server/src/provider/acp/AntigravityAcpSupport.ts +++ b/apps/server/src/provider/acp/AntigravityAcpSupport.ts @@ -31,6 +31,7 @@ export interface AntigravityAcpRuntimeInput extends Omit< | "authenticateEagerly" | "cancelBehavior" | "clientCapabilities" + | "clientInfo" | "onStderr" | "resumeMethod" | "transformSessionUpdate" @@ -38,7 +39,7 @@ export interface AntigravityAcpRuntimeInput extends Omit< > { readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; readonly onAuthorizationUrl?: (url: string) => Effect.Effect; - readonly clientFileSystem?: boolean; + readonly clientFileReads?: boolean; readonly authMethod?: AntigravityAuthMethod; } @@ -56,11 +57,11 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( authenticateEagerly: true, resumeMethod: "resume", cancelBehavior: "wait-for-prompt", + // The agent offers non-Gemini models only to allowlisted clients such as Zed. + clientInfo: { name: "zed", version: "0.0.0" }, clientCapabilities: { - fs: { - readTextFile: input.clientFileSystem === true, - writeTextFile: input.clientFileSystem === true, - }, + // Advertising writes swaps the agent's diff edits for whole-file client_edit_file. + fs: { readTextFile: input.clientFileReads === true, writeTextFile: false }, terminal: false, }, transformStdout: makeAntigravityStdoutTransform( @@ -91,7 +92,7 @@ export function antigravityPermissionMode(runtimeMode: RuntimeMode): string { } } -function antigravityModelOptions( +export function antigravityModelOptions( configOptions: ReadonlyArray, ) { const model = configOptions.find((option) => option.id === "model"); @@ -99,6 +100,28 @@ function antigravityModelOptions( return model.options.flatMap((entry) => ("value" in entry ? [entry] : entry.options)); } +/** Runtime 1.4 reports thinking as its own option; 1.3 encoded it in the model ID. */ +export function antigravityThinkingConfig( + configOptions: ReadonlyArray | null | undefined, +) { + const option = configOptions?.find((candidate) => candidate.category === "thought_level"); + return option?.type === "select" ? option : undefined; +} + +/** + * Runtime 1.4 lists `gemini-3.8-flash` where 1.3 listed `gemini-3.8-flash-high`. The agent + * still accepts the old IDs, so pass them through; the current model may have no thinking option. + */ +function legacyAntigravityFamily( + configOptions: ReadonlyArray, + model: string, +) { + return antigravityModelOptions(configOptions) + .map((option) => option.value) + .filter((family) => model.startsWith(`${family}-`)) + .toSorted((left, right) => right.length - left.length)[0]; +} + function resolveAntigravityModel(input: { readonly configOptions: ReadonlyArray; readonly model: string | null | undefined; @@ -108,7 +131,9 @@ function resolveAntigravityModel(input: { const current = modelConfig?.type === "select" ? modelConfig.currentValue : undefined; if (input.model && input.model !== ANTIGRAVITY_DEFAULT_MODEL) return input.model; const options = antigravityModelOptions(input.configOptions); - return input.defaultModel && options.some((option) => option.value === input.defaultModel) + return input.defaultModel && + (options.some((option) => option.value === input.defaultModel) || + legacyAntigravityFamily(input.configOptions, input.defaultModel) !== undefined) ? input.defaultModel : current; } @@ -118,7 +143,7 @@ export const applyAntigravityAcpModelSelection = Effect.fn("applyAntigravityAcpM function* (input: { readonly runtime: Pick< AcpSessionRuntime.AcpSessionRuntime["Service"], - "getConfigOptions" | "setModel" + "getConfigOptions" | "setModel" | "setConfigOption" >; readonly model: string | null | undefined; readonly defaultModel?: string | undefined; @@ -135,6 +160,15 @@ export const applyAntigravityAcpModelSelection = Effect.fn("applyAntigravityAcpM const explicit = Boolean(input.model) && input.model !== ANTIGRAVITY_DEFAULT_MODEL; if (resolved === undefined || (!explicit && resolved === current)) return current; const options = antigravityModelOptions(configOptions); + const legacyFamily = options.some((option) => option.value === resolved) + ? undefined + : legacyAntigravityFamily(configOptions, resolved); + if (legacyFamily !== undefined) { + yield* input.runtime + .setConfigOption("model", resolved, { allowUnlistedValue: true }) + .pipe(Effect.mapError(input.mapError)); + return legacyFamily; + } if (!options.some((option) => option.value === resolved)) { return yield* Effect.fail( input.mapError( diff --git a/apps/server/src/provider/acp/AntigravityClientFiles.ts b/apps/server/src/provider/acp/AntigravityClientFiles.ts index 4efbc28a80ce2..c6079234d916c 100644 --- a/apps/server/src/provider/acp/AntigravityClientFiles.ts +++ b/apps/server/src/provider/acp/AntigravityClientFiles.ts @@ -5,7 +5,6 @@ import type * as Path from "effect/Path"; import * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpSchema from "effect-acp/compat"; -/** The agent requests permission for every write; check only path containment here. */ const CLIENT_FILE_MAX_BYTES = 8 * 1024 * 1024; function isInsideRoot(path: Path.Path, root: string, candidate: string): boolean { @@ -98,21 +97,3 @@ export const readAntigravityClientTextFile = Effect.fn("AntigravityClientFiles.r return { content: lines.slice(start, end).join("\n") }; }, ); - -export const writeAntigravityClientTextFile = Effect.fn("AntigravityClientFiles.writeTextFile")( - function* (input: { - readonly fileSystem: FileSystem.FileSystem; - readonly path: Path.Path; - readonly allowedRoots: ReadonlyArray; - readonly request: EffectAcpSchema.WriteTextFileRequest; - }): Effect.fn.Return { - const filePath = yield* resolveClientFilePath({ ...input, requestPath: input.request.path }); - yield* input.fileSystem.makeDirectory(input.path.dirname(filePath), { recursive: true }).pipe( - Effect.andThen(input.fileSystem.writeFileString(filePath, input.request.content)), - Effect.mapError(() => - EffectAcpErrors.AcpRequestError.internalError(`Could not write '${input.request.path}'.`), - ), - ); - return {}; - }, -); diff --git a/apps/server/src/provider/acp/AntigravityProtocol.test.ts b/apps/server/src/provider/acp/AntigravityProtocol.test.ts index 192c8b43ffb14..66c83e0c48249 100644 --- a/apps/server/src/provider/acp/AntigravityProtocol.test.ts +++ b/apps/server/src/provider/acp/AntigravityProtocol.test.ts @@ -24,7 +24,12 @@ const isSessionNotification = Schema.is(AcpWireSchema.SessionNotification); describe("native Antigravity subagent tools", () => { it("recognizes only native invocation titles and excludes MCP tools", () => { const toolCall = { toolCallId: "trajectory:4", kind: "other", data: {} }; - for (const title of ["Running start_subagent", "Run start_subagent?"]) { + for (const title of [ + "Running start_subagent", + "Run start_subagent?", + "Running invoke_subagent", + "Run invoke_subagent?", + ]) { expect(classifyAntigravitySubagentToolCall({ ...toolCall, title }, {})).toBe("subagent"); expect( classifyAntigravitySubagentToolCall( diff --git a/apps/server/src/provider/acp/AntigravityProtocol.ts b/apps/server/src/provider/acp/AntigravityProtocol.ts index 7e2f24a31d3b9..ac65db6373add 100644 --- a/apps/server/src/provider/acp/AntigravityProtocol.ts +++ b/apps/server/src/provider/acp/AntigravityProtocol.ts @@ -324,14 +324,22 @@ export function normalizeAntigravityToolCall(toolCall: AcpToolCallState): AcpToo }; } -/** ACP 1.1.1 reports subagent calls as ordinary tools without child IDs or models. */ +const ANTIGRAVITY_SUBAGENT_TITLES = new Set([ + "Running start_subagent", + "Run start_subagent?", + "Running invoke_subagent", + "Run invoke_subagent?", +]); + +/** The ACP agent reports subagent calls as ordinary tools without child IDs or models. */ export function classifyAntigravitySubagentToolCall( toolCall: AcpToolCallState, rawPayload?: unknown, ): "subagent" | "mcp" | undefined { if ( (toolCall.kind !== undefined && toolCall.kind !== "other") || - (toolCall.title !== "Running start_subagent" && toolCall.title !== "Run start_subagent?") + toolCall.title === undefined || + !ANTIGRAVITY_SUBAGENT_TITLES.has(toolCall.title) ) return undefined; const update = Predicate.isObject(rawPayload) ? rawPayload.update : undefined; diff --git a/apps/server/src/provider/antigravityRelease.ts b/apps/server/src/provider/antigravityRelease.ts index f00185865b05a..17173760d5e3d 100644 --- a/apps/server/src/provider/antigravityRelease.ts +++ b/apps/server/src/provider/antigravityRelease.ts @@ -1,4 +1,5 @@ -const ANTIGRAVITY_RELEASE_VERSION = "1.3.0"; +// 1.4.2 through 1.4.4 write skills/system/run-workflow/SKILL.md into every session workspace. +const ANTIGRAVITY_RELEASE_VERSION = "1.4.1"; export interface AntigravityReleaseAsset { readonly version: string; @@ -20,66 +21,66 @@ const releaseAssets = new Map([ "darwin-arm64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.3.0-darwin-arm64.zip", - sha256: "7cd97045f7b4fe81175a107cdf16f9c51484e3c78a5162cae415338bb6aa5b88", - archiveBytes: 111_456_962, - executable: { name: "agy_acp_server.par", bytes: 278_535_456 }, - harness: { name: "localharness_external", bytes: 118_611_392 }, + url: "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.4.1-darwin-arm64.zip", + sha256: "f9bdd68291ae78c38d2141e63bc86b84e6cc7b9f60f71652c68435a7943deb17", + archiveBytes: 112_737_434, + executable: { name: "agy_acp_server.par", bytes: 280_996_576 }, + harness: { name: "localharness_external", bytes: 120_246_928 }, }, ], [ "darwin-x64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.3.0-darwin-x86_64.zip", - sha256: "bb23956b89984bf5d354af2c3725e6c57f0cc1b7228e77a0e91c9c2bc1d47646", - archiveBytes: 117_245_544, - executable: { name: "agy_acp_server.par", bytes: 282_840_688 }, - harness: { name: "localharness_external", bytes: 124_175_392 }, + url: "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.4.1-darwin-x86_64.zip", + sha256: "27e31f096874704f14167b07ef9e84cc77134615ee41d024f7b1c957add85307", + archiveBytes: 118_349_036, + executable: { name: "agy_acp_server.par", bytes: 285_186_624 }, + harness: { name: "localharness_external", bytes: 124_910_048 }, }, ], [ "linux-x64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.3.0-linux-x86_64.zip", - sha256: "9fb60956af0a9d76220a4db91ca9ac88e2a2372ad68f985ab5fceace6b825b96", - archiveBytes: 333_727_150, - executable: { name: "agy_acp_server.par", bytes: 926_533_965 }, - harness: { name: "localharness_external", bytes: 130_388_040 }, + url: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.4.1-linux-x86_64.zip", + sha256: "2e37012ceb2a5c7fc7309b6e6596234fb637ee3c62a5199f65d4cbe777871384", + archiveBytes: 336_170_339, + executable: { name: "agy_acp_server.par", bytes: 930_826_753 }, + harness: { name: "localharness_external", bytes: 131_187_512 }, }, ], [ "linux-arm64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.3.0-linux-arm64.zip", - sha256: "500b0bc0fb858e88f4df404d4cedf80bf9298c178291e39e383d6c50b111cbdf", - archiveBytes: 321_690_363, - executable: { name: "agy_acp_server.par", bytes: 930_848_992 }, - harness: { name: "localharness_external", bytes: 123_224_968 }, + url: "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.4.1-linux-arm64.zip", + sha256: "609336a1c97545487e3ddd55dbd153dc0f40f4e6534486cc1c5f4c0ebb85c2da", + archiveBytes: 321_922_507, + executable: { name: "agy_acp_server.par", bytes: 933_416_796 }, + harness: { name: "localharness_external", bytes: 124_895_000 }, }, ], [ "win32-x64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/windows/agy-acp-server-1.3.0-windows-x86_64.zip", - sha256: "65215e0688681fa3116e048a9eab27ef53af1bbd6f3da3f1c52bd4911d8b17f9", - archiveBytes: 124_509_787, - executable: { name: "agy_acp_server.exe", bytes: 81_437_336 }, - harness: { name: "localharness_external.exe", bytes: 145_548_952 }, + url: "https://dl.google.com/agy-extensions/releases/windows/agy-acp-server-1.4.1-windows-x86_64.zip", + sha256: "5f5b7d0328df17c128b2be338d4567b0d24e56ace8196e88794fec37d1372554", + archiveBytes: 129_599_308, + executable: { name: "agy_acp_server.exe", bytes: 86_098_784 }, + harness: { name: "localharness_external.exe", bytes: 146_036_888 }, }, ], [ "win32-arm64", { version: ANTIGRAVITY_RELEASE_VERSION, - url: "https://dl.google.com/agy-extensions/releases/windows/agy-acp-server-1.3.0-windows-arm64.zip", - sha256: "4a0f469720e9beb9438a979f543fdbfad5022ebe0992c052c590bd78b3144ca3", - archiveBytes: 124_654_803, - executable: { name: "agy_acp_server.exe", bytes: 85_893_472 }, - harness: { name: "localharness_external.exe", bytes: 135_640_216 }, + url: "https://dl.google.com/agy-extensions/releases/windows/agy-acp-server-1.4.1-windows-arm64.zip", + sha256: "24c52bbc12aae20f55eb53d373669412d95a9c44fd59be4ea4e355918b91fbf7", + archiveBytes: 132_507_344, + executable: { name: "agy_acp_server.exe", bytes: 93_197_744 }, + harness: { name: "localharness_external.exe", bytes: 136_970_904 }, }, ], ]); diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index b42932fcc6668..87ed61776ea9c 100644 --- a/apps/server/src/provider/model-manifest.json +++ b/apps/server/src/provider/model-manifest.json @@ -75,8 +75,12 @@ { "driver": "antigravity", "t3CodeRange": ">=0.0.42", - "recommendedRange": "=1.1.1", - "ranges": [{ "range": "=1.1.1", "status": "supported" }] + "recommendedRange": "=1.4.1", + "ranges": [ + { "range": "=1.4.1", "status": "supported" }, + { "range": "=1.3.0", "status": "supported" }, + { "range": "=1.1.1", "status": "supported" } + ] }, { "driver": "pi", diff --git a/apps/server/src/provider/providerCompatibility.test.ts b/apps/server/src/provider/providerCompatibility.test.ts index e3d4f672016c0..12c541fbd776d 100644 --- a/apps/server/src/provider/providerCompatibility.test.ts +++ b/apps/server/src/provider/providerCompatibility.test.ts @@ -21,7 +21,12 @@ import { ProviderCompatibilityPolicy, resolveProviderCompatibility, } from "./providerCompatibility.ts"; +import { resolveAntigravityReleaseAsset } from "./antigravityRelease.ts"; +import bundledManifest from "./model-manifest.json" with { type: "json" }; +const decodeCompatibilityPolicies = Schema.decodeUnknownSync( + Schema.Array(ProviderCompatibilityPolicy), +); const driver = ProviderDriverKind.make("codex"); const policy: ProviderCompatibilityPolicy = { driver, @@ -127,6 +132,19 @@ describe("provider compatibility", () => { } }); + it("supports the managed Antigravity runtime in the bundled manifest", () => { + const policies = decodeCompatibilityPolicies(bundledManifest.compatibility); + const release = resolveAntigravityReleaseAsset("darwin", "arm64"); + assert.strictEqual( + resolveProviderCompatibility( + policies, + ProviderDriverKind.make("antigravity"), + `agy_acp_server_${release?.version}`, + )?.status, + "supported", + ); + }); + it("classifies boundaries and treats unlisted versions and release tags as unknown", () => { for (const [version, expected] of [ ["0.9.9", "broken"], diff --git a/apps/server/src/textGeneration/AntigravityTextGeneration.test.ts b/apps/server/src/textGeneration/AntigravityTextGeneration.test.ts index 395a24eeedc8d..3fcafcc151789 100644 --- a/apps/server/src/textGeneration/AntigravityTextGeneration.test.ts +++ b/apps/server/src/textGeneration/AntigravityTextGeneration.test.ts @@ -170,6 +170,7 @@ const makeFixture = Effect.fn("makeAntigravityTextGenerationFixture")(function* Effect.sync(() => { state.selectedModels.push(model); }), + setConfigOption: () => Effect.succeed({ configOptions: [] }), prompt: (request) => Effect.gen(function* () { state.prompts.push(request); diff --git a/apps/server/src/textGeneration/AntigravityTextGeneration.ts b/apps/server/src/textGeneration/AntigravityTextGeneration.ts index 670c2eb28d513..921a2f118e34a 100644 --- a/apps/server/src/textGeneration/AntigravityTextGeneration.ts +++ b/apps/server/src/textGeneration/AntigravityTextGeneration.ts @@ -31,6 +31,7 @@ type AntigravityTextRuntime = Pick< | "getConfigOptions" | "getEvents" | "setModel" + | "setConfigOption" | "prompt" | "cancel" | "handleSessionUpdate" diff --git a/docs/user/providers-antigravity.md b/docs/user/providers-antigravity.md index 0b1cd2f2c6da1..76bbc8266a941 100644 --- a/docs/user/providers-antigravity.md +++ b/docs/user/providers-antigravity.md @@ -68,9 +68,15 @@ Leave the field blank to use the managed runtime, or a compatible executable on ## Models and threads -The model list comes from your Antigravity account and can differ from other -Antigravity apps. A resumed thread keeps its selected model. If access to that -model ends, select another available model before continuing. +The model list comes from your Antigravity account, including Claude and GPT models +when your plan has them. Pick a thinking level in the composer's effort menu; it +shows only the levels the selected model supports. A resumed thread keeps its +selected model. If access to that model ends, select another available model +before continuing. + +Antigravity creates empty `.gemini/skills` and `.agents/skills` folders in the +workspace when a session starts. Git does not track empty folders, so they never +appear in commits. Use Antigravity's native `/plan` command for planning. T3 Code's separate Plan mode is unavailable. Tool approvals follow [Permission modes](./permission-modes.md). diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 1cdbd20f5508b..398eac2170e70 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -288,7 +288,7 @@ export interface AcpAdapterV2Flavor { request: EffectAcpSchema.ReadTextFileRequest, cwd: string | null, ) => Effect.Effect; - readonly writeTextFile: ( + readonly writeTextFile?: ( request: EffectAcpSchema.WriteTextFileRequest, cwd: string | null, ) => Effect.Effect; @@ -1993,7 +1993,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( clientCapabilities: { fs: { readTextFile: flavor.clientFileSystem !== undefined, - writeTextFile: flavor.clientFileSystem !== undefined, + writeTextFile: flavor.clientFileSystem?.writeTextFile !== undefined, }, terminal: clientTerminals !== undefined, elicitation: { form: {}, ...(flavor.onUrlElicitation ? { url: {} } : {}) }, @@ -5958,11 +5958,14 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( Effect.flatMap(({ policy }) => clientFileSystem.readTextFile(request, policy.cwd)), ), ); - yield* targetRuntime.handleWriteTextFile((request) => - clientPolicyContext.pipe( - Effect.flatMap(({ policy }) => clientFileSystem.writeTextFile(request, policy.cwd)), - ), - ); + const writeTextFile = clientFileSystem.writeTextFile; + if (writeTextFile !== undefined) { + yield* targetRuntime.handleWriteTextFile((request) => + clientPolicyContext.pipe( + Effect.flatMap(({ policy }) => writeTextFile(request, policy.cwd)), + ), + ); + } } if (handlerOptions.mcp !== false) { yield* wireAcpRuntimeMcpHandlers(targetRuntime, runtimeMcpBridge); From d7935f135f50f3798c1e247ebe8b88587dd62adf Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 03:28:56 -0700 Subject: [PATCH 15/25] feat(web): choose what the thread details card shows (#26904) Co-authored-by: Claude Opus 5.5 --- .../settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/ChatView.tsx | 19 +- apps/web/src/components/GitActionsControl.tsx | 25 +- .../chat/OpenInPicker.browser.test.tsx | 13 + .../components/chat/OpenInPickerShortcut.ts | 40 ++- .../chat/ThreadAutomationsPanel.tsx | 12 +- .../chat/ThreadDetailsCard.browser.test.tsx | 26 +- .../src/components/chat/ThreadDetailsCard.tsx | 16 +- .../chat/ThreadDetailsItemsMenu.tsx | 45 +++ .../components/chat/ThreadDetailsPanel.tsx | 262 ++++++++++++------ .../components/chat/ThreadDetailsSection.tsx | 7 +- .../components/chat/ThreadDetailsUsage.tsx | 2 +- .../chat/threadDetailsItems.test.ts | 35 +++ .../src/components/chat/threadDetailsItems.ts | 60 ++++ .../settings/AppearanceSettingsPanel.tsx | 2 + .../components/settings/LabsSettingsPanel.tsx | 21 -- .../settings/ThreadDetailsItemsRow.tsx | 53 ++++ .../src/components/settings/settingsSearch.ts | 14 +- docs/user/thread-sidebar.md | 5 + docs/user/usage.md | 2 +- packages/contracts/src/settings.ts | 7 + 21 files changed, 514 insertions(+), 153 deletions(-) create mode 100644 apps/web/src/components/chat/ThreadDetailsItemsMenu.tsx create mode 100644 apps/web/src/components/chat/threadDetailsItems.test.ts create mode 100644 apps/web/src/components/chat/threadDetailsItems.ts create mode 100644 apps/web/src/components/settings/ThreadDetailsItemsRow.tsx diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index c80498c14c1fb..ec5062f9105bf 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -65,6 +65,7 @@ const clientSettings: ClientSettings = { boringSidebarEnabled: false, boringSidebarMultilineRows: false, threadDetailsUsageEnabled: false, + threadDetailsItems: {}, legacySidebarEnabled: false, sidebarWorkingShelfEnabled: false, loadBalancingEnabled: false, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index b9104836b62ee..de76f47844bfa 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1228,9 +1228,6 @@ export default function ChatView(props: ChatViewProps) { const canChangeEnvironment = hasMultipleEnvironments && !envLocked && draftId !== null && !draftThread?.promotedTo; - const threadDetailsUsageEnabled = useClientSettings( - (settings) => settings.threadDetailsUsageEnabled, - ); const loadBalancingSettings = useClientSettings((settings) => ({ loadBalancingEnabled: settings.loadBalancingEnabled, loadBalancingWeights: settings.loadBalancingWeights, @@ -3773,16 +3770,12 @@ export default function ChatView(props: ChatViewProps) { onAddProjectScript={saveProjectScriptStable} onUpdateProjectScript={updateProjectScriptStable} onDeleteProjectScript={deleteProjectScriptStable} - usage={ - threadDetailsUsageEnabled - ? { - providerInstanceId: activeProviderInstanceId, - providers: providerStatuses, - sources: serverConfig?.usageLimitSources ?? EMPTY_USAGE_LIMIT_SOURCES, - contextWindow: activeContextWindow, - } - : undefined - } + usage={{ + providerInstanceId: activeProviderInstanceId, + providers: providerStatuses, + sources: serverConfig?.usageLimitSources ?? EMPTY_USAGE_LIMIT_SOURCES, + contextWindow: activeContextWindow, + }} /> void; } @@ -926,6 +928,8 @@ export default function GitActionsControl({ activeThreadRef, draftId, compact = false, + showActions = true, + showChanges = true, onOpenChanges, }: GitActionsControlProps) { const panelAnchorRef = useRef(null); @@ -1476,7 +1480,7 @@ export default function GitActionsControl({ return ( <> - {!isRepo ? ( + {!showActions ? null : !isRepo ? ( )} - {isRepo && !quietEmptyDraft ? ( + {showChanges && isRepo && !quietEmptyDraft ? ( - + {/* Its discovery query should stop when the Git actions row that opens it is hidden. */} + {showActions ? ( + + ) : null} { }); }); +describe("remote editor links", () => { + it("opens the link from the fallback when the picker is not mounted", async () => { + await using fixture = await setup(); + const { screen, pressOpenFavorite, FavoriteEditorFallback } = fixture; + state.remote = { mode: "remote-links", host: { kind: "ssh-alias", host: "sol" } }; + await act(async () => screen.rerender()); + + expect((await pressOpenFavorite()).defaultPrevented).toBe(true); + expect(state.openUrl).toHaveBeenCalledWith(expect.stringContaining("ssh-remote+sol")); + expect(state.run).not.toHaveBeenCalled(); + }); +}); + describe("open in editor announcement", () => { it("speaks only after the shortcut opened the editor, from the picker or the fallback", async () => { await using fixture = await setup(); diff --git a/apps/web/src/components/chat/OpenInPickerShortcut.ts b/apps/web/src/components/chat/OpenInPickerShortcut.ts index 8c8249c67b1a0..833c3f3784987 100644 --- a/apps/web/src/components/chat/OpenInPickerShortcut.ts +++ b/apps/web/src/components/chat/OpenInPickerShortcut.ts @@ -1,11 +1,16 @@ -import type { EditorId, EnvironmentId, ResolvedKeybindingsConfig } from "@t3tools/contracts"; +import { + buildRemoteOpenUrl, + type EditorId, + type EnvironmentId, + type ResolvedKeybindingsConfig, +} from "@t3tools/contracts"; import { useEffect } from "react"; import { usePreferredEditor } from "../../editorPreferences"; import { editorLabelForPlatform } from "../../editorLabels"; import { isOpenFavoriteEditorShortcut } from "../../keybindings"; import { announce } from "../../lib/announce"; -import { useRemoteOpenState } from "../../remoteOpen"; +import { openRemoteEditorUrl, useRemoteCapableEditors, useRemoteOpenState } from "../../remoteOpen"; import { registerCommands } from "../../shortcuts/dispatcher"; import { shellEnvironment } from "../../state/shell"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -49,18 +54,39 @@ export function useOpenFavoriteEditorShortcut({ openInCwd: string | null; }) { const openInEditorMutation = useAtomCommand(shellEnvironment.openInEditor, "open in editor"); - const [preferredEditor] = usePreferredEditor(availableEditors); const remote = useRemoteOpenState(environmentId); + const remoteCapableEditors = useRemoteCapableEditors(); + // Matches the picker: remote editors run on the viewing machine. + const [preferredEditor] = usePreferredEditor( + remote.mode === "local-exec" ? availableEditors : remoteCapableEditors, + ); - // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes a window keydown listener for the open-in-editor shortcut while enabled locally + // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes a window keydown listener for the open-in-editor shortcut while enabled useEffect(() => { - if (!enabled || remote.mode !== "local-exec") return; + if (!enabled || remote.mode === "remote-unavailable") return; const handler = (event: globalThis.KeyboardEvent) => { - if (event.defaultPrevented || !isOpenFavoriteEditorShortcut(event, keybindings)) return; + if ( + event.repeat || + event.defaultPrevented || + !isOpenFavoriteEditorShortcut(event, keybindings) + ) + return; if (!openInCwd || !preferredEditor) return; event.preventDefault(); const label = editorLabelForPlatform(preferredEditor, navigator.platform); + if (remote.mode === "remote-links") { + const url = buildRemoteOpenUrl({ + editor: preferredEditor, + ...remote.host, + absolutePath: openInCwd, + }); + if (url === undefined) return; + void openRemoteEditorUrl(url).then((opened) => { + if (opened) announce(`Opened in ${label}`); + }); + return; + } void openInEditorMutation({ environmentId, input: { @@ -80,6 +106,6 @@ export function useOpenFavoriteEditorShortcut({ openInCwd, openInEditorMutation, preferredEditor, - remote.mode, + remote, ]); } diff --git a/apps/web/src/components/chat/ThreadAutomationsPanel.tsx b/apps/web/src/components/chat/ThreadAutomationsPanel.tsx index 20ecde3272276..ca2497bd20ee8 100644 --- a/apps/web/src/components/chat/ThreadAutomationsPanel.tsx +++ b/apps/web/src/components/chat/ThreadAutomationsPanel.tsx @@ -153,12 +153,6 @@ export function ThreadAutomationsPanel(props: { } > - {tasksQuery.error !== null ? ( -

- Could not load automations: {tasksQuery.error} -

- ) : null} -
    {boundTasks.map((task) => (
  • ))}
+ {/* Kept last: the details card treats a section whose last child is empty as empty. */} + {tasksQuery.error !== null ? ( +

+ Could not load automations: {tasksQuery.error} +

+ ) : null} ); } diff --git a/apps/web/src/components/chat/ThreadDetailsCard.browser.test.tsx b/apps/web/src/components/chat/ThreadDetailsCard.browser.test.tsx index 37110875ed0ea..a8728d7879dd4 100644 --- a/apps/web/src/components/chat/ThreadDetailsCard.browser.test.tsx +++ b/apps/web/src/components/chat/ThreadDetailsCard.browser.test.tsx @@ -42,10 +42,13 @@ function Card({ threadRef, inline, fixture, + rows = 1, }: { threadRef: ScopedThreadRef; inline: boolean; fixture: ReturnType; + /** Rows of 100px shown at full density. Compact always takes 50px. */ + rows?: number; }) { return ( // The web root renders in StrictMode, which re-runs ref callbacks on every mount. @@ -63,8 +66,17 @@ function Card({ detailsCardPlacement: inline ? placement : null, }} > - - {() =>

Details for {threadRef.threadId}

} + + {(density) => ( +

+ Details for {threadRef.threadId} +

+ )}
@@ -136,3 +148,13 @@ it("reopens after a normal close", async () => { await setOpen(true); await expect.element(page.getByText("Details for thread-b")).toBeVisible(); }); + +it("returns to full density when hidden items make the content fit", async () => { + using fixture = setup(); + await using screen = await render(); + const card = page.getByRole("region", { name: "Thread details" }); + await expect.element(card).toHaveAttribute("data-density", "compact"); + + await screen.rerender(); + await expect.element(card).toHaveAttribute("data-density", "full"); +}); diff --git a/apps/web/src/components/chat/ThreadDetailsCard.tsx b/apps/web/src/components/chat/ThreadDetailsCard.tsx index 4add56b80eb08..a5df136f14615 100644 --- a/apps/web/src/components/chat/ThreadDetailsCard.tsx +++ b/apps/web/src/components/chat/ThreadDetailsCard.tsx @@ -1,5 +1,11 @@ import { workspaceAreaProps } from "../../lib/workspaceFocus"; -import { useLayoutEffect, useState, type ReactNode, type RefObject } from "react"; +import { + useLayoutEffect, + useState, + type MouseEventHandler, + type ReactNode, + type RefObject, +} from "react"; import type { ScopedThreadRef } from "@t3tools/contracts"; import { ScrollArea } from "../ui/scroll-area"; import { cn } from "../../lib/utils"; @@ -17,11 +23,16 @@ export function ThreadDetailsCard({ threadRef, anchor, handle, + contentKey, + onContextMenu, children, }: { threadRef: ScopedThreadRef; anchor: RefObject; handle: ReturnType; + /** Changes when the set of rendered items changes, so cached heights are measured again. */ + contentKey: string; + onContextMenu?: MouseEventHandler; children: (density: "full" | "compact" | "essential") => ReactNode; }) { const detailsArea = workspaceAreaProps("details", 3, "Thread details"); @@ -36,7 +47,7 @@ export function ThreadDetailsCard({ const popoverOpen = useRightPanelStore((state) => selectThreadPanelOpen(state.threadPanelVisibilityByThreadKey, threadRef, "popover"), ); - const measurementKey = `${threadRef.environmentId}:${threadRef.threadId}:${preferredPlacement?.width ?? "popup"}`; + const measurementKey = `${threadRef.environmentId}:${threadRef.threadId}:${preferredPlacement?.width ?? "popup"}:${contentKey}`; const [measurements, setMeasurements] = useState({ key: measurementKey, heights: { full: 0, compact: 0 }, @@ -109,6 +120,7 @@ export function ThreadDetailsCard({ "max-h-[min(calc(100dvh-6.5rem),calc(var(--available-height,100dvh)-1rem))]", )} style={placement ? { maxHeight: height } : undefined} + onContextMenu={onContextMenu} data-thread-details-card > diff --git a/apps/web/src/components/chat/ThreadDetailsItemsMenu.tsx b/apps/web/src/components/chat/ThreadDetailsItemsMenu.tsx new file mode 100644 index 0000000000000..d84c13809eaf6 --- /dev/null +++ b/apps/web/src/components/chat/ThreadDetailsItemsMenu.tsx @@ -0,0 +1,45 @@ +import { + getClientSettings, + updateClientSettings, + useClientSettings, +} from "../../hooks/useSettings"; +import { MenuCheckboxItem, MenuGroup, MenuGroupLabel, MenuItem, MenuSeparator } from "../ui/menu"; +import { + isDefaultThreadDetailsVisibility, + resolveThreadDetailsVisibility, + RESET_THREAD_DETAILS_ITEMS, + setThreadDetailsItem, + THREAD_DETAILS_ITEMS, +} from "./threadDetailsItems"; + +/** Menu body that shows and hides thread details items. The card's context menu and Settings share it. */ +export function ThreadDetailsItemsMenuItems() { + const visibility = useClientSettings(resolveThreadDetailsVisibility); + return ( + <> + + Show in thread details + {THREAD_DETAILS_ITEMS.map((item) => ( + + updateClientSettings({ + threadDetailsItems: setThreadDetailsItem(getClientSettings(), item, checked), + }) + } + > + {item.label} + + ))} + + + updateClientSettings(RESET_THREAD_DETAILS_ITEMS)} + > + Reset to default + + + ); +} diff --git a/apps/web/src/components/chat/ThreadDetailsPanel.tsx b/apps/web/src/components/chat/ThreadDetailsPanel.tsx index 77a6d15763904..35a954db9d43e 100644 --- a/apps/web/src/components/chat/ThreadDetailsPanel.tsx +++ b/apps/web/src/components/chat/ThreadDetailsPanel.tsx @@ -16,7 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput, type ProjectScriptActionResult, } from "../ProjectScriptsControl"; -import { memo, type ComponentProps } from "react"; +import { memo, useState, type ComponentProps } from "react"; import { ThreadDetailsCard } from "./ThreadDetailsCard"; import { OpenInPicker } from "./OpenInPicker"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; @@ -24,6 +24,17 @@ import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl"; import { ThreadServersPanel } from "./ThreadServersPanel"; import { ThreadDetailsUsage, type ThreadDetailsUsageInput } from "./ThreadDetailsUsage"; +import { ThreadDetailsItemsMenuItems } from "./ThreadDetailsItemsMenu"; +import { resolveThreadDetailsVisibility, THREAD_DETAILS_ITEMS } from "./threadDetailsItems"; +import { useClientSettings } from "../../hooks/useSettings"; +import { cn } from "../../lib/utils"; +import { SlidersHorizontalIcon } from "lucide-react"; +import { ThreadDetailsControl } from "./ThreadDetailsControl"; +import { + THREAD_DETAILS_PANEL_ICON_CLASS, + THREAD_DETAILS_PANEL_LABEL_CLASS, +} from "./threadDetailsPanelStyles"; +import { Menu, MenuPopup } from "../ui/menu"; const GitActionsControl = memo(BaseGitActionsControl); @@ -68,14 +79,15 @@ interface ThreadDetailsPanelProps extends Pick< input: NewProjectScriptInput, ) => Promise; onDeleteProjectScript: (scriptId: string) => Promise; - /** Set only while the Labs usage readout is on. */ - usage?: ThreadDetailsUsageInput | undefined; + usage: ThreadDetailsUsageInput; } export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { + const visible = useClientSettings(resolveThreadDetailsVisibility); + const [menuPosition, setMenuPosition] = useState<{ x: number; y: number } | null>(null); const fileScripts = useT3ProjectFileScripts( props.environmentId, - props.activeProjectScripts ? props.gitCwd : null, + props.activeProjectScripts && visible.scripts ? props.gitCwd : null, ); const activeThreadRef = { environmentId: props.environmentId, threadId: props.threadId }; const branchToolbarProps = { @@ -109,99 +121,181 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { threadRef={{ environmentId: props.environmentId, threadId: props.threadId }} anchor={props.anchor} handle={props.handle} + contentKey={THREAD_DETAILS_ITEMS.filter((item) => visible[item.id]) + .map((item) => item.id) + .join(",")} + onContextMenu={(event) => { + // Popups the card opens bubble here through React; they, fields, links, and selected text + // keep their own menus. + const target = event.target; + const selection = window.getSelection(); + if ( + !(target instanceof Element) || + !event.currentTarget.contains(target) || + target.closest("input, textarea, [contenteditable], a[href]") || + (selection && + !selection.isCollapsed && + (event.currentTarget.contains(selection.anchorNode) || + event.currentTarget.contains(selection.focusNode))) + ) + return; + event.preventDefault(); + setMenuPosition({ x: event.clientX, y: event.clientY }); + }} > - {(density) => ( - <> - -
- {density === "full" ? ( - + {(density) => { + // Section flags follow the rows that will render, so no empty section steals the divider rule. + const show = { + environment: density === "full" && visible.environment, + openIn: density !== "essential" && visible.openIn && props.showOpenInPicker, + scripts: visible.scripts && props.activeProjectScripts !== undefined, + branch: props.gitCwd !== null && props.isGitRepo && visible.branch, + git: + props.gitCwd !== null && + props.activeProjectName !== undefined && + (visible.gitActions || (visible.changes && props.isGitRepo)), + }; + return ( + <> +
+ {show.environment || show.openIn || show.scripts ? ( + +
+ {show.environment ? ( + + ) : null} + + {show.openIn ? ( + + ) : null} + + {show.scripts && props.activeProjectScripts ? ( + + ) : null} +
+
+ ) : null} + + {props.gitCwd && (show.branch || show.git) ? ( + +
+ {show.branch ? ( + + ) : null} + {show.git ? ( + + ) : null} +
+
+ ) : null} + + {density === "full" && !props.draftId && visible.servers ? ( + ) : null} - {density !== "essential" && props.showOpenInPicker ? ( - ) : null} - {props.activeProjectScripts ? ( - ) : null} -
- - {props.gitCwd ? ( - -
- {props.isGitRepo ? ( - - ) : null} - {props.activeProjectName ? ( - - ) : null} -
-
- ) : null} - - {density === "full" && !props.draftId ? ( - - ) : null} - - {density === "full" && !props.draftId ? ( - - ) : null} + {visible.usage ? : null} - {density === "full" && !props.draftId ? ( - - ) : null} + {/* Shown only when no section above has content, so an emptied card can be refilled. */} +
+ { + const rect = event.currentTarget.getBoundingClientRect(); + setMenuPosition({ x: rect.left, y: rect.bottom }); + }} + > + + + Choose what to show + + +
+
- {props.usage ? : null} - - )} + !open && setMenuPosition(null)} + > + + new DOMRect(menuPosition.x, menuPosition.y, 0, 0), + } + : undefined + } + align="start" + side="bottom" + > + + + + + ); + }}
); } diff --git a/apps/web/src/components/chat/ThreadDetailsSection.tsx b/apps/web/src/components/chat/ThreadDetailsSection.tsx index 00aacd5a7b152..de5ce6c63825d 100644 --- a/apps/web/src/components/chat/ThreadDetailsSection.tsx +++ b/apps/web/src/components/chat/ThreadDetailsSection.tsx @@ -22,7 +22,12 @@ export function ThreadDetailsSection({ {...props} aria-labelledby={showHeading ? headingId : undefined} aria-label={showHeading ? undefined : title} - className={cn("px-2 pt-2 pb-2.5", separated && "border-t border-border/65")} + // A row can still render nothing (a fresh draft quiets Git actions), so an empty section + // collapses, and a divider only follows a section that has content. + className={cn( + "px-2 pt-2 pb-2.5 has-[>div:last-child:empty]:hidden", + separated && "border-border/65 [section:not(:has(>div:last-child:empty))~&]:border-t", + )} >
item.id === "usage")!; +const servers = THREAD_DETAILS_ITEMS.find((item) => item.id === "servers")!; + +describe("thread details items", () => { + it.each([ + { legacy: false, items: {}, usage: false }, + { legacy: true, items: {}, usage: true }, + { legacy: true, items: { usage: false }, usage: false }, + { legacy: false, items: { usage: true, retired: false }, usage: true }, + ])("resolves Usage from overrides and the retired Labs flag: %o", (row) => { + const visibility = resolveThreadDetailsVisibility({ + threadDetailsItems: row.items, + threadDetailsUsageEnabled: row.legacy, + }); + expect(visibility.usage).toBe(row.usage); + expect(visibility.servers).toBe(true); + }); + + it("stores only choices that differ from the default", () => { + const legacyOn = { threadDetailsItems: {}, threadDetailsUsageEnabled: true }; + expect(setThreadDetailsItem(legacyOn, usage, false)).toEqual({ usage: false }); + expect(setThreadDetailsItem(legacyOn, usage, true)).toEqual({}); + + const hidden = { threadDetailsItems: { servers: false }, threadDetailsUsageEnabled: false }; + expect(setThreadDetailsItem(hidden, servers, true)).toEqual({}); + }); +}); diff --git a/apps/web/src/components/chat/threadDetailsItems.ts b/apps/web/src/components/chat/threadDetailsItems.ts new file mode 100644 index 0000000000000..7502a4c0b1dce --- /dev/null +++ b/apps/web/src/components/chat/threadDetailsItems.ts @@ -0,0 +1,60 @@ +import type { ClientSettings } from "@t3tools/contracts"; + +// Ids are stored in client settings, so never rename one. +export const THREAD_DETAILS_ITEMS = [ + { id: "environment", label: "Environment and worktree", defaultVisible: true }, + { id: "openIn", label: "Open in editor", defaultVisible: true }, + { id: "scripts", label: "Project scripts", defaultVisible: true }, + { id: "branch", label: "Branch and pull request", defaultVisible: true }, + { id: "gitActions", label: "Git actions", defaultVisible: true }, + { id: "changes", label: "Changes", defaultVisible: true }, + { id: "servers", label: "Servers", defaultVisible: true }, + { id: "automations", label: "Automations", defaultVisible: true }, + { id: "lineage", label: "Lineage", defaultVisible: true }, + { id: "usage", label: "Usage", defaultVisible: false }, +] as const; + +export type ThreadDetailsItemId = (typeof THREAD_DETAILS_ITEMS)[number]["id"]; +export type ThreadDetailsVisibility = Readonly>; + +type ThreadDetailsItem = (typeof THREAD_DETAILS_ITEMS)[number]; +type ThreadDetailsItemSettings = Pick< + ClientSettings, + "threadDetailsItems" | "threadDetailsUsageEnabled" +>; + +// People who turned on the retired Labs toggle keep Usage until they change it here. +function defaultVisible(item: ThreadDetailsItem, settings: ThreadDetailsItemSettings) { + return item.id === "usage" ? settings.threadDetailsUsageEnabled : item.defaultVisible; +} + +export function resolveThreadDetailsVisibility( + settings: ThreadDetailsItemSettings, +): ThreadDetailsVisibility { + return Object.fromEntries( + THREAD_DETAILS_ITEMS.map((item) => [ + item.id, + settings.threadDetailsItems[item.id] ?? defaultVisible(item, settings), + ]), + ) as Record; +} + +/** Stores only choices that differ from the default, so a later default change reaches everyone else. */ +export function setThreadDetailsItem( + settings: ThreadDetailsItemSettings, + item: ThreadDetailsItem, + visible: boolean, +): ClientSettings["threadDetailsItems"] { + const { [item.id]: _previous, ...rest } = settings.threadDetailsItems; + return visible === defaultVisible(item, settings) ? rest : { ...rest, [item.id]: visible }; +} + +export function isDefaultThreadDetailsVisibility(visibility: ThreadDetailsVisibility) { + return THREAD_DETAILS_ITEMS.every((item) => visibility[item.id] === item.defaultVisible); +} + +/** Also clears the retired Labs flag, so Usage returns to off. */ +export const RESET_THREAD_DETAILS_ITEMS = { + threadDetailsItems: {}, + threadDetailsUsageEnabled: false, +} satisfies Partial; diff --git a/apps/web/src/components/settings/AppearanceSettingsPanel.tsx b/apps/web/src/components/settings/AppearanceSettingsPanel.tsx index f47ee3f52c793..1c5812a2bda0d 100644 --- a/apps/web/src/components/settings/AppearanceSettingsPanel.tsx +++ b/apps/web/src/components/settings/AppearanceSettingsPanel.tsx @@ -34,6 +34,7 @@ import { searchableSetting } from "./settingsSearch"; import { AdvancedSettings } from "./AdvancedSettings"; import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; import { TypographySection } from "./TypographySettings"; +import { ThreadDetailsItemsRow } from "./ThreadDetailsItemsRow"; const ENVIRONMENT_IDENTIFICATION_LABELS: Record = { artwork: "Artwork", @@ -273,6 +274,7 @@ export function AppearanceSettingsPanel() {
} /> + - {/* Clerk is only mounted when this build has T3 Connect. */} {hasCloudPublicConfig() ? : } @@ -70,26 +69,6 @@ function BoringSidebarMultilineRow() { ); } -function ThreadDetailsUsageRow() { - const settings = useScopedSettings(); - const updateSettings = useUpdateScopedSettings(); - return ( - - updateSettings({ threadDetailsUsageEnabled: Boolean(checked) }) - } - aria-label="Usage in thread details" - /> - } - /> - ); -} - /** Signed out of T3 Connect, the row offers to sign in instead of its switch. */ function GuestLinksRowWithSignIn() { const { isSignedIn } = useAuth(); diff --git a/apps/web/src/components/settings/ThreadDetailsItemsRow.tsx b/apps/web/src/components/settings/ThreadDetailsItemsRow.tsx new file mode 100644 index 0000000000000..a142155d0357d --- /dev/null +++ b/apps/web/src/components/settings/ThreadDetailsItemsRow.tsx @@ -0,0 +1,53 @@ +import { ChevronDownIcon } from "lucide-react"; + +import { updateClientSettings, useClientSettings } from "../../hooks/useSettings"; +import { ThreadDetailsItemsMenuItems } from "../chat/ThreadDetailsItemsMenu"; +import { + isDefaultThreadDetailsVisibility, + resolveThreadDetailsVisibility, + RESET_THREAD_DETAILS_ITEMS, + THREAD_DETAILS_ITEMS, +} from "../chat/threadDetailsItems"; +import { Button } from "../ui/button"; +import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; +import { SettingResetButton, SettingsRow } from "./settingsLayout"; +import { searchableSetting } from "./settingsSearch"; + +export function ThreadDetailsItemsRow() { + const visibility = useClientSettings(resolveThreadDetailsVisibility); + const enabled = THREAD_DETAILS_ITEMS.filter((item) => visibility[item.id]).length; + return ( + updateClientSettings(RESET_THREAD_DETAILS_ITEMS)} + /> + ) + } + control={ + + + } + > + {enabled} of {THREAD_DETAILS_ITEMS.length} on + + + + + + + } + /> + ); +} diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 0d87e9bd32c83..1d5c13e72a061 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -315,6 +315,14 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/appearance", searchTerms: ["wide full width column layout messages composer monitor"], }, + { + id: "thread-details-items", + title: "Thread details", + to: "/settings/appearance", + searchTerms: [ + "customize show hide panel card environment editor scripts branch pull request git changes servers automations lineage usage limits 5 hour weekly quota tokens context window", + ], + }, { id: "effort-display", title: "Effort display", @@ -437,12 +445,6 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/labs", searchTerms: ["beta boring sidebar folder rows multiline branch pull request details"], }, - { - id: "thread-details-usage", - title: "Usage in thread details", - to: "/settings/labs", - searchTerms: ["beta limits 5 hour weekly context window tokens quota rate limit"], - }, { id: "guest-links", title: "Guest links", diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index dfc7b1245ef21..fb5e12b08372b 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -277,6 +277,11 @@ Select a server to open it in the browser panel. **Stop** ends the server's job and keeps the agent, the terminal, and T3 Code running. Stop needs terminal access to the environment and is not available on Windows. +To choose what the thread details card shows, right-click the card or open +**Settings → Appearance → Thread details**. Hidden rows keep their keyboard +shortcuts, such as Open in editor and project scripts. **Reset to default** brings +back the standard rows. + ## Snooze until later To change the choices in the snooze menu, open **Settings → Threads → Snooze options**. diff --git a/docs/user/usage.md b/docs/user/usage.md index 8064380c5e717..155175351c550 100644 --- a/docs/user/usage.md +++ b/docs/user/usage.md @@ -116,7 +116,7 @@ current model's limits without leaving the conversation. The result opens above closes when you dismiss it or send your next message. It uses the same snapshot as **Usage → Limits**, so it does not run the agent or refresh anything. The command is offered only for providers that appear under **Usage → Limits**. -To keep them in view, turn on **Settings → Labs → Usage in thread details** on web and desktop. +To keep them in view on web and desktop, turn on **Usage** in **Settings → Appearance → Thread details**, or right-click the thread details card. The bottom of the thread details card then shows how much is left in each limit window of the current provider account and in the thread's context window. Some windows, such as a weekly Opus limit, count only one model. Hover a row to see when it resets, your pace, or diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index d789770f1616a..16eb51aa1769c 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -477,7 +477,13 @@ export const ClientSettingsSchema = Schema.Struct({ boringSidebarMultilineRows: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), ), + // Retired Labs toggle. Still read as the default for the Usage item in threadDetailsItems. threadDetailsUsageEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + // Sparse show/hide overrides for thread details card items. Missing ids use the client's + // default, so new items appear for everyone and unknown ids survive a downgrade. + threadDetailsItems: Schema.Record(Schema.String, Schema.Boolean).pipe( + Schema.withDecodingDefault(Effect.succeed({})), + ), // Opt-in for the removed legacy sidebar. Still read so those users land in // the boring sidebar; the boring sidebar toggle clears it. legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), @@ -1722,6 +1728,7 @@ export const ClientSettingsPatch = Schema.Struct({ boringSidebarEnabled: Schema.optionalKey(Schema.Boolean), boringSidebarMultilineRows: Schema.optionalKey(Schema.Boolean), threadDetailsUsageEnabled: Schema.optionalKey(Schema.Boolean), + threadDetailsItems: Schema.optionalKey(Schema.Record(Schema.String, Schema.Boolean)), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarWorkingShelfEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), From f2943ee90ef76b1db3fc1c1cd3808252418f5850 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 29 Oct 2026 03:54:18 -0700 Subject: [PATCH 16/25] feat(updates): switch your whole fleet between Stable and Nightly in one step (#26288) Co-authored-by: Claude Opus 5.5 --- apps/desktop/src/ipc/methods/updates.ts | 18 +- apps/desktop/src/preload.ts | 7 +- .../src/settings/DesktopAppSettings.ts | 4 +- .../src/updates/DesktopRemoteUpdates.test.ts | 269 ++++++++++++++++++ .../src/updates/DesktopRemoteUpdates.ts | 94 +++++- .../src/updates/DesktopUpdates.test.ts | 170 +++++++++++ apps/desktop/src/updates/DesktopUpdates.ts | 92 +++++- apps/desktop/src/updates/releaseNotes.ts | 4 +- apps/desktop/src/updates/updateChannels.ts | 7 - .../desktop/src/updates/updatesTestHarness.ts | 29 +- apps/server/src/cloud/selfUpdate.test.ts | 33 +++ apps/server/src/cloud/selfUpdate.ts | 42 ++- .../server/src/cloud/servicePreflight.test.ts | 58 ++++ apps/server/src/cloud/servicePreflight.ts | 26 ++ .../desktopUpdate/DesktopAppUpdate.test.ts | 18 ++ .../src/desktopUpdate/DesktopAppUpdate.ts | 18 +- .../src/environment/ServerEnvironment.ts | 5 +- .../server/src/persistence/Migrations.test.ts | 20 ++ .../src/persistence/databaseMigrationHead.ts | 49 ++++ .../DesktopTelemetryReceiver.ts | 11 +- .../ServerUpdateAction.browser.test.tsx | 25 ++ .../web/src/components/ServerUpdateAction.tsx | 53 +++- .../components/settings/SettingsPanels.tsx | 104 ++++--- .../settings/UpdateTrackSwitchDialog.tsx | 178 ++++++++++++ .../settings/updateTrackSwitch.logic.test.ts | 76 +++++ .../settings/updateTrackSwitch.logic.ts | 50 ++++ .../components/sidebar/SidebarUpdatePill.tsx | 7 +- .../sidebar/UpdateEverythingDialog.tsx | 32 ++- .../sidebar/updateEverything.logic.test.ts | 167 ++++++++++- .../sidebar/updateEverything.logic.ts | 216 +++++++++++++- .../components/sidebar/useUpdateEverything.ts | 54 ++-- apps/web/src/state/npmRelease.ts | 42 ++- docs/user/updating.md | 23 ++ packages/client-runtime/src/state/server.ts | 11 + packages/contracts/src/environment.ts | 4 + packages/contracts/src/ipc.ts | 9 +- packages/contracts/src/resourceTelemetry.ts | 5 +- packages/contracts/src/server.ts | 5 + packages/shared/src/cliRelease.ts | 42 +++ scripts/build-npm-launcher.test.ts | 3 + scripts/build-npm-launcher.ts | 23 +- 41 files changed, 1959 insertions(+), 144 deletions(-) create mode 100644 apps/server/src/persistence/databaseMigrationHead.ts create mode 100644 apps/web/src/components/settings/UpdateTrackSwitchDialog.tsx create mode 100644 apps/web/src/components/settings/updateTrackSwitch.logic.test.ts create mode 100644 apps/web/src/components/settings/updateTrackSwitch.logic.ts diff --git a/apps/desktop/src/ipc/methods/updates.ts b/apps/desktop/src/ipc/methods/updates.ts index b2212609030d1..ca85ee8b0c417 100644 --- a/apps/desktop/src/ipc/methods/updates.ts +++ b/apps/desktop/src/ipc/methods/updates.ts @@ -23,11 +23,14 @@ export const getUpdateState = DesktopIpc.makeIpcMethod({ export const setUpdateChannel = DesktopIpc.makeIpcMethod({ channel: IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, - payload: DesktopUpdateChannelSchema, + payload: Schema.Struct({ + channel: DesktopUpdateChannelSchema, + minimumVersion: Schema.optional(Schema.String), + }), result: DesktopUpdateStateSchema, - handler: Effect.fn("desktop.ipc.updates.setChannel")(function* (channel) { + handler: Effect.fn("desktop.ipc.updates.setChannel")(function* ({ channel, minimumVersion }) { const updates = yield* DesktopUpdates.DesktopUpdates; - return yield* updates.setChannel(channel); + return yield* updates.setChannel(channel, { minimumVersion }); }), }); @@ -43,11 +46,14 @@ export const downloadUpdate = DesktopIpc.makeIpcMethod({ export const installUpdate = DesktopIpc.makeIpcMethod({ channel: IpcChannels.UPDATE_INSTALL_CHANNEL, - payload: Schema.Void, + /** A version installs only that downloaded build, never one that replaced it. */ + payload: Schema.UndefinedOr(Schema.String), result: DesktopUpdateActionResultSchema, - handler: Effect.fn("desktop.ipc.updates.install")(function* () { + handler: Effect.fn("desktop.ipc.updates.install")(function* (expectedVersion) { const updates = yield* DesktopUpdates.DesktopUpdates; - return yield* updates.install; + if (expectedVersion === undefined) return yield* updates.install; + const { accepted, completed, state } = yield* updates.installPrepared(expectedVersion); + return { accepted, completed, state }; }), }); diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index a3e1e711cb2d3..965b7db5d6d40 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -261,11 +261,12 @@ contextBridge.exposeInMainWorld("desktopBridge", { }; }, getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL), - setUpdateChannel: (channel) => - ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel), + setUpdateChannel: (channel, minimumVersion) => + ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, { channel, minimumVersion }), checkForUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_CHECK_CHANNEL), downloadUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_DOWNLOAD_CHANNEL), - installUpdate: () => ipcRenderer.invoke(IpcChannels.UPDATE_INSTALL_CHANNEL), + installUpdate: (expectedVersion) => + ipcRenderer.invoke(IpcChannels.UPDATE_INSTALL_CHANNEL, expectedVersion), cliCommand: { getState: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_GET_STATE_CHANNEL), install: () => ipcRenderer.invoke(IpcChannels.CLI_COMMAND_INSTALL_CHANNEL), diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 5767057b4a35d..a0624ced669ea 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -22,7 +22,7 @@ import { normalizeLinuxPasswordStorePreference, type LinuxPasswordStorePreference, } from "../linuxSecretStorage.ts"; -import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts"; +import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; import { isValidDistroName } from "../wsl/wslPathParsing.ts"; export interface DesktopSettings { @@ -180,7 +180,7 @@ export class DesktopAppSettings extends Context.Service< export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings { return { ...DEFAULT_DESKTOP_SETTINGS, - updateChannel: resolveDefaultDesktopUpdateChannel(appVersion), + updateChannel: desktopUpdateChannelOf(appVersion), }; } diff --git a/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts b/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts index 253a015d8ec9e..1f5321148b77b 100644 --- a/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopRemoteUpdates.test.ts @@ -119,6 +119,239 @@ describe("DesktopRemoteUpdates", () => { ); }); + it.effect("switches to the requested track before preparing its release", () => { + // In-memory settings keep the channel write off real disk I/O. + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* Queue.offer(requests, { + ...request("req-switch"), + channel: "nightly", + targetVersion: "1.2.4-nightly.20261027.1", + }); + yield* settle; + assert.equal((yield* updates.getState).channel, "nightly"); + + harness.emit("update-available", { version: "1.2.4-nightly.20261027.1" }); + yield* settle; + harness.emit("update-downloaded", { version: "1.2.4-nightly.20261027.1" }); + yield* settle; + + const terminals = terminalReports(reports); + assert.deepEqual( + terminals.map((report) => [report.requestId, report.outcome]), + [["req-switch", "ready-to-install"]], + ); + assert.equal(terminals[0]?.state.downloadedVersion, "1.2.4-nightly.20261027.1"); + }), + ); + }); + + it.effect( + "refuses a crossing whose feed has only an older build, and never offers it later", + () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + const older = "1.2.4-nightly.20261020.1"; + yield* Queue.offer(requests, { + ...request("req-older"), + channel: "nightly", + targetVersion: "1.2.4-nightly.20261027.1", + }); + yield* settle; + // Answers the track change's check, then the request's own check. + harness.emit("update-available", { version: older }); + yield* settle; + harness.emit("update-available", { version: older }); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => [report.outcome, report.reason]), + [["failed", "The desktop app found no build of 1.2.4-nightly.20261027.1."]], + ); + // The saved track is the user's intent; a retry picks it up from here. + assert.equal((yield* updates.getState).channel, "nightly"); + + // A plain update afterwards cannot reach the build the switch refused. + yield* Queue.offer(requests, request("req-plain")); + yield* settle; + harness.emit("update-available", { version: older }); + yield* settle; + assert.isNull((yield* updates.getState).availableVersion); + assert.equal(harness.downloadCount(), 0); + }), + ); + }, + ); + + it.effect("only saves the track on a host already running the release", () => { + const harness = makeHarness({ savedChannel: "nightly" }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* Queue.offer(requests, { + ...request("req-track-only"), + channel: "latest", + targetVersion: "1.2.3", + }); + yield* settle; + // Even with a newer build on the feed, the host only saves its track. + harness.emit("update-available", { version: "1.2.4" }); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => report.outcome), + ["up-to-date"], + ); + assert.equal((yield* updates.getState).channel, "latest"); + assert.equal(harness.downloadCount(), 0); + }), + ); + }); + + it.effect("drops a retained download below the switch's checked release", () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + // Downloaded earlier on this Stable host, before the switch checked 1.2.5. + harness.emit("update-downloaded", { version: "1.2.4" }); + yield* settle; + assert.equal((yield* updates.getState).downloadedVersion, "1.2.4"); + + yield* Queue.offer(requests, { + ...request("req-floor"), + channel: "latest", + targetVersion: "1.2.5", + }); + yield* settle; + harness.emit("update-available", { version: "1.2.5" }); + yield* settle; + harness.emit("update-downloaded", { version: "1.2.5" }); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => [ + report.outcome, + report.state.downloadedVersion, + ]), + [["ready-to-install", "1.2.5"]], + ); + }), + ); + }); + + it.effect("refuses to cross tracks remotely while WSL backends need their own checks", () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void, wslBackend: true }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* Queue.offer(requests, { + ...request("req-wsl"), + channel: "nightly", + targetVersion: "1.2.4-nightly.20261027.1", + }); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => report.outcome), + ["failed"], + ); + assert.include(terminalReports(reports)[0]?.reason ?? "", "Switch it on that machine"); + assert.equal((yield* updates.getState).channel, "latest"); + }), + ); + }); + + it.effect("fails a switch that leaves a same-track host below the checked release", () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + yield* Queue.offer(requests, { + ...request("req-behind"), + channel: "latest", + targetVersion: "1.2.5", + }); + yield* settle; + // The feed lags npm and has nothing newer than this host's 1.2.3. + harness.emit("update-not-available"); + yield* settle; + harness.emit("update-not-available"); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => [report.outcome, report.reason]), + [["failed", "The desktop app found no build of 1.2.5."]], + ); + }), + ); + }); + + it.effect("fails a crossing to an older-numbered Stable that the feed does not offer", () => { + const harness = makeHarness({ + appVersion: "1.2.4-nightly.20261027.1", + beforeSetUpdateChannel: Effect.void, + }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + yield* Queue.offer(requests, { + ...request("req-to-stable"), + channel: "latest", + targetVersion: "1.2.3", + }); + yield* settle; + harness.emit("update-not-available"); + yield* settle; + harness.emit("update-not-available"); + yield* settle; + + // Still on Nightly, though the target is numbered below this build. + assert.deepEqual( + terminalReports(reports).map((report) => [report.outcome, report.reason]), + [["failed", "The desktop app found no build of 1.2.3."]], + ); + }), + ); + }); + + it.effect("accepts a newer build than the checked release on the requested track", () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return runRemoteUpdatesTest(harness, ({ reports, requests }) => + Effect.gen(function* () { + const newer = "1.2.4-nightly.20261028.2"; + yield* Queue.offer(requests, { + ...request("req-newer"), + channel: "nightly", + targetVersion: "1.2.4-nightly.20261027.1", + }); + yield* settle; + harness.emit("update-available", { version: newer }); + yield* settle; + harness.emit("update-downloaded", { version: newer }); + yield* settle; + + assert.deepEqual( + terminalReports(reports).map((report) => [ + report.outcome, + report.state.downloadedVersion, + ]), + [["ready-to-install", newer]], + ); + }), + ); + }); + it.effect("reports a failed outcome when quitAndInstall fails", () => { const harness = makeHarness({ quitAndInstall: Effect.fail( @@ -525,6 +758,42 @@ describe("DesktopRemoteUpdates", () => { ); }); + it.effect("cancels a track switch while its own feed check is still running", () => { + const releaseCheck = Deferred.makeUnsafe(); + const harness = makeHarness({ + beforeSetUpdateChannel: Effect.void, + checkForUpdates: Deferred.await(releaseCheck), + }); + + return runRemoteUpdatesTest(harness, ({ requests, cancellations }) => + Effect.gen(function* () { + yield* Queue.offer(requests, { + ...request("req-switch-cancel"), + channel: "nightly", + targetVersion: "1.2.4-nightly.20261027.1", + }); + yield* settle; + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* Queue.offer(cancellations, { + version: 1, + type: "cancelDesktopUpdate", + requestId: "req-switch-cancel", + }); + yield* settle; + // The next request starts without waiting for the cancelled switch's feed check. + yield* Queue.offer(requests, { + ...request("req-switch-back"), + channel: "latest", + targetVersion: "1.2.3", + }); + yield* settle; + + assert.equal((yield* updates.getState).channel, "latest"); + yield* Deferred.succeed(releaseCheck, undefined); + }), + ); + }); + it.effect("cancels an active preparation so the next request can run", () => { const releaseCheck = Deferred.makeUnsafe(); const harness = makeHarness({ checkForUpdates: Deferred.await(releaseCheck) }); diff --git a/apps/desktop/src/updates/DesktopRemoteUpdates.ts b/apps/desktop/src/updates/DesktopRemoteUpdates.ts index cb606d8fbd253..560e2e78d4bb1 100644 --- a/apps/desktop/src/updates/DesktopRemoteUpdates.ts +++ b/apps/desktop/src/updates/DesktopRemoteUpdates.ts @@ -10,10 +10,13 @@ import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; +import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; +import { compareSemverVersions } from "@t3tools/shared/semver"; import type * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as DesktopObservability from "../app/DesktopObservability.ts"; +import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts"; import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts"; import * as DesktopUpdates from "./DesktopUpdates.ts"; import { @@ -44,9 +47,13 @@ type CommitClaim = export const listen: Effect.Effect< void, never, - DesktopUpdates.DesktopUpdates | DesktopTelemetryPublisher.DesktopTelemetryPublisher | Scope.Scope + | DesktopUpdates.DesktopUpdates + | DesktopTelemetryPublisher.DesktopTelemetryPublisher + | DesktopBackendPool.DesktopBackendPool + | Scope.Scope > = Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; + const backends = yield* DesktopBackendPool.DesktopBackendPool; const publisher = yield* DesktopTelemetryPublisher.DesktopTelemetryPublisher; const activeRequestIdRef = yield* Ref.make(Option.none()); const requestControlRef = yield* Ref.make<{ @@ -171,7 +178,59 @@ export const listen: Effect.Effect< return; } yield* Ref.set(activeRequestIdRef, Option.some(request.requestId)); - yield* logInfo("remote update requested", { requestId: request.requestId }); + yield* logInfo("remote update requested", { + requestId: request.requestId, + channel: request.channel, + }); + // A switch saves its track first, so the step machine never sees the old track's offer. + // The saved track is the user's intent and stays even if this request fails: a track saved + // toward Stable never offers an older build outside a guarded switch. + if (request.channel !== undefined) { + // A reinstall restarts every backend, but the requesting server checked only its own + // database. A WSL backend keeps its own, so cross tracks only where all can be checked. + const current = yield* updates.getState; + if ( + desktopUpdateChannelOf(current.currentVersion) !== request.channel && + (yield* backends.list).length > 1 + ) { + yield* publishReport( + current, + { + outcome: "failed", + reason: + "This desktop app also runs WSL backends, whose databases a remote switch cannot check. Switch it on that machine.", + }, + request.requestId, + ); + return; + } + const switched = yield* Effect.raceFirst( + updates.setChannel(request.channel, { minimumVersion: request.targetVersion }).pipe( + Effect.as(true), + Effect.catch((error) => + updates.getState.pipe( + Effect.flatMap((state) => + publishReport( + state, + { outcome: "failed", reason: error.message }, + request.requestId, + ), + ), + Effect.as(false), + ), + ), + ), + Deferred.await(cancellation).pipe(Effect.as(false)), + ); + if (!switched) return; + // A host already running the target only needed its track saved; installing a newer + // build here would restart it without the dialog having said so. + const state = yield* updates.getState; + if (request.targetVersion === state.currentVersion) { + yield* publishReport(state, { outcome: "up-to-date" }, request.requestId); + return; + } + } const { latest, changes } = yield* updates.subscribe; const disabledReason = Option.getOrNull(yield* updates.disabledReason); let attempts: RemoteDesktopUpdateAttempts = { checks: 0, downloads: 0 }; @@ -184,6 +243,12 @@ export const listen: Effect.Effect< Effect.forkScoped, ); + // A host leaving its build's track may need an older build, down to the checked release; + // the updater offers nothing below it. + const crossing = (state: DesktopUpdateState) => + request.channel !== undefined && + request.targetVersion !== undefined && + desktopUpdateChannelOf(state.currentVersion) !== request.channel; const step = Effect.fn("DesktopRemoteUpdates.step")(function* ( state: DesktopUpdateState, ): Effect.fn.Return { @@ -193,7 +258,7 @@ export const listen: Effect.Effect< return false; case "check": attempts = { ...attempts, checks: attempts.checks + 1 }; - yield* updates.check("remote-update").pipe( + yield* updates.check("remote-update", { allowDowngrade: crossing(state) }).pipe( Effect.flatMap((result) => { if (result.checked) return Effect.void; attempts = { ...attempts, checks: attempts.checks - 1 }; @@ -246,21 +311,34 @@ export const listen: Effect.Effect< yield* logInfo("remote update prepared", { requestId: request.requestId }); return true; } - case "done": + case "done": { + // A switch that leaves the host on its old track, or below its checked release, did + // not reach it; a track-only host already at the release is done. + const done = + request.targetVersion !== undefined && + next.outcome === "up-to-date" && + (crossing(state) || + compareSemverVersions(state.currentVersion, request.targetVersion) < 0) + ? { + outcome: "failed" as const, + reason: `The desktop app found no build of ${request.targetVersion}.`, + } + : next; yield* publishReport( state, { - outcome: next.outcome, - ...(next.reason === undefined ? {} : { reason: next.reason }), + outcome: done.outcome, + ...(done.reason === undefined ? {} : { reason: done.reason }), }, request.requestId, ); yield* logInfo("remote update finished", { requestId: request.requestId, - outcome: next.outcome, - reason: next.reason ?? null, + outcome: done.outcome, + reason: done.reason ?? null, }); return true; + } } }); diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index d2f24143fc209..c6bcd46b8c206 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest"; import { DESKTOP_UPDATE_RESTART_MARKER_FILE } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; @@ -694,6 +695,65 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("ignores a finished download from a track it no longer follows", () => { + const harness = makeHarness(); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + // A backed-out switch's download keeps reporting progress until it finishes. + harness.emit("download-progress", { percent: 40 }); + yield* flushCallbacks; + harness.emit("update-downloaded", { version: "1.2.4-nightly.20261027.1" }); + yield* flushCallbacks; + + const state = yield* updates.getState; + assert.equal(state.downloadedVersion, null); + assert.notEqual(state.status, "downloading"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("offers older releases only to checks that leave the running build's track", () => { + const harness = makeHarness({ beforeSetUpdateChannel: Effect.void }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + const before = harness.checkAllowedDowngrade.length; + yield* updates.setChannel("nightly"); + yield* updates.setChannel("latest"); + yield* updates.check("remote-update", { allowDowngrade: true }); + + // Back on its own Stable track the build never needs an older release. + assert.deepEqual(harness.checkAllowedDowngrade.slice(before), [true, false, true]); + assert.isFalse(harness.allowDowngrade()); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("re-checks a saved track that differs from the running build's track", () => { + const harness = makeHarness({ + appVersion: "1.2.4-nightly.20261027.1", + beforeSetUpdateChannel: Effect.void, + }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.setChannel("latest"); + const checks = harness.checkCount(); + + yield* updates.setChannel("latest"); + + assert.equal(harness.checkCount(), checks + 1); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + it.effect("does not persist an unchanged update channel as a user preference", () => { const harness = makeHarness(); @@ -713,6 +773,116 @@ describe("DesktopUpdates", () => { ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); }); + it.effect("offers nothing below a track switch's checked release", () => { + const harness = makeHarness({ + appVersion: "1.2.4-nightly.20261027.1", + beforeSetUpdateChannel: Effect.void, + }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.setChannel("latest", { minimumVersion: "1.2.3" }); + + harness.emit("update-available", { version: "1.2.2" }); + yield* flushCallbacks; + assert.isNull((yield* updates.getState).availableVersion); + harness.emit("update-downloaded", { version: "1.2.2" }); + yield* flushCallbacks; + assert.isNull((yield* updates.getState).downloadedVersion); + + harness.emit("update-available", { version: "1.2.3" }); + yield* flushCallbacks; + assert.equal((yield* updates.getState).availableVersion, "1.2.3"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("keeps a saved Stable track across a restart without offering an older release", () => { + // A Nightly build whose switch to Stable did not finish before the app quit. + const harness = makeHarness({ appVersion: "1.2.4-nightly.20261027.1", savedChannel: "latest" }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.check("startup"); + + assert.equal((yield* updates.getState).channel, "latest"); + assert.deepEqual(harness.checkAllowedDowngrade, [false]); + // Semver ranks Stable 1.2.4 above this Nightly, but a Nightly built before the release's + // version bump can hold migrations 1.2.4 lacks. Crossing needs a guarded switch. + harness.emit("update-available", { version: "1.2.4" }); + yield* flushCallbacks; + assert.isNull((yield* updates.getState).availableVersion); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("keeps a saved Nightly track across a restart without crossing on its own", () => { + // A Stable build whose switch to Nightly was refused before the app quit; its floor is gone. + const harness = makeHarness({ savedChannel: "nightly" }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.check("startup"); + harness.emit("update-available", { version: "1.2.4-nightly.20261027.1" }); + yield* flushCallbacks; + + assert.equal((yield* updates.getState).channel, "nightly"); + assert.isNull((yield* updates.getState).availableVersion); + assert.deepEqual(harness.checkAllowedDowngrade, [false]); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("never offers an older Nightly to a Nightly build's own checks", () => { + const harness = makeHarness({ appVersion: "1.2.4-nightly.20261027.1" }); + + return Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.check("poll"); + + assert.equal((yield* updates.getState).channel, "nightly"); + assert.deepEqual(harness.checkAllowedDowngrade, [false]); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }); + + it.effect("waits out a background check instead of refusing a track change", () => + Effect.gen(function* () { + const checkStarted = yield* Deferred.make(); + const releaseCheck = yield* Deferred.make(); + const harness = makeHarness({ + beforeSetUpdateChannel: Effect.void, + checkForUpdates: Deferred.succeed(checkStarted, undefined).pipe( + Effect.andThen(Deferred.await(releaseCheck)), + ), + }); + + yield* Effect.scoped( + Effect.gen(function* () { + const updates = yield* DesktopUpdates.DesktopUpdates; + yield* updates.configure; + yield* updates.check("poll").pipe(Effect.forkScoped); + yield* Deferred.await(checkStarted); + + const channelFiber = yield* updates.setChannel("nightly").pipe(Effect.forkScoped); + yield* Deferred.succeed(releaseCheck, undefined); + yield* TestClock.adjust(Duration.seconds(2)); + const state = yield* Fiber.join(channelFiber); + + assert.equal(state.channel, "nightly"); + }), + ).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer))); + }), + ); + it.effect("rejects checks while an update channel change is being persisted", () => Effect.gen(function* () { const channelChangeStarted = yield* Deferred.make(); diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index aeda73f2bfad3..4da742f453aa6 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -19,6 +19,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PubSub from "effect/PubSub"; import * as Ref from "effect/Ref"; +import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; @@ -34,7 +35,8 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as IpcChannels from "../ipc/channels.ts"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import { normalizeDesktopUpdateReleaseNotes } from "./releaseNotes.ts"; -import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts"; +import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; +import { compareSemverVersions } from "@t3tools/shared/semver"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, @@ -49,6 +51,8 @@ import { resolveNotAppliedInstallVersion, } from "./updateMachine.ts"; +const CHANNEL_CHANGE_RETRY_DELAY = Duration.seconds(1); +const CHANNEL_CHANGE_RETRIES = 30; const AUTO_UPDATE_STARTUP_DELAY = "15 seconds"; const AUTO_UPDATE_POLL_INTERVAL = "4 minutes"; const PREPARED_INSTALL_CHECK_WAIT = Duration.seconds(90); @@ -179,10 +183,16 @@ export class DesktopUpdates extends Context.Service< readonly emitState: Effect.Effect; readonly disabledReason: Effect.Effect>; readonly configure: Effect.Effect; + /** `minimumVersion` is a track switch's checked release; nothing older is offered after. */ readonly setChannel: ( channel: DesktopUpdateChannel, + options?: { readonly minimumVersion?: string | undefined }, ) => Effect.Effect; - readonly check: (reason: string) => Effect.Effect; + /** `allowDowngrade` is for a guarded track switch, which checked that older release. */ + readonly check: ( + reason: string, + options?: { readonly allowDowngrade?: boolean }, + ) => Effect.Effect; readonly download: Effect.Effect; readonly install: Effect.Effect; readonly installPrepared: ( @@ -351,6 +361,20 @@ export const make = Effect.gen(function* () { const updaterConfiguredRef = yield* Ref.make(false); const lastLoggedDownloadMilestoneRef = yield* Ref.make(-1); const notAppliedVersionRef = yield* Ref.make(Option.none()); + // The release a track switch's database check cleared. A build crosses to another track only + // through such a switch, and never below that release: version order alone cannot tell whether + // a Stable knows a Nightly's migrations. + const releaseFloorRef = yield* Ref.make(Option.none()); + const offerable = (version: string, state: DesktopUpdateState) => + Ref.get(releaseFloorRef).pipe( + Effect.map( + (floor) => + desktopUpdateChannelOf(version) === state.channel && + (Option.isSome(floor) + ? compareSemverVersions(version, floor.value) >= 0 + : desktopUpdateChannelOf(environment.appVersion) === state.channel), + ), + ); const updateStateRef = yield* Ref.make( createInitialDesktopUpdateState( environment.appVersion, @@ -454,12 +478,13 @@ export const make = Effect.gen(function* () { const allowsPrerelease = channel === "nightly"; yield* electronUpdater.setChannel(channel); yield* electronUpdater.setAllowPrerelease(allowsPrerelease); - yield* electronUpdater.setAllowDowngrade(allowsPrerelease); + // An older build can lack migrations this one ran. Only a guarded track switch's own check + // may offer one. + yield* electronUpdater.setAllowDowngrade(false); yield* electronUpdater.setFullChangelog(allowsPrerelease); yield* logUpdaterInfo("using update channel", { channel, allowPrerelease: allowsPrerelease, - allowDowngrade: allowsPrerelease, fullChangelog: allowsPrerelease, }); }); @@ -467,6 +492,7 @@ export const make = Effect.gen(function* () { const checkForUpdates = Effect.fn("desktop.updates.checkForUpdates")(function* ( reason: string, actionReservation: "acquire" | "held" = "acquire", + allowDowngrade = false, ) { yield* Effect.annotateCurrentSpan({ reason }); if (yield* Ref.get(desktopState.quitting)) return false; @@ -488,7 +514,11 @@ export const make = Effect.gen(function* () { yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt)); yield* logUpdaterInfo("checking for updates", { reason }); + // The flag is shared updater state, so it flips only while this check holds the reservation. + const restoreDowngrade = yield* electronUpdater.allowDowngrade; + if (allowDowngrade) yield* electronUpdater.setAllowDowngrade(true); return yield* electronUpdater.checkForUpdates.pipe( + Effect.ensuring(electronUpdater.setAllowDowngrade(restoreDowngrade).pipe(Effect.ignore)), Effect.as(true), Effect.catchTags({ ElectronUpdaterCheckForUpdatesError: Effect.fn( @@ -849,8 +879,8 @@ export const make = Effect.gen(function* () { Effect.flatMap( Effect.fn("desktop.updates.applyUpdateAvailable")(function* (info) { const state = yield* Ref.get(updateStateRef); - if (resolveDefaultDesktopUpdateChannel(info.version) !== state.channel) { - yield* logUpdaterInfo("ignoring update that does not match selected channel", { + if (!(yield* offerable(info.version, state))) { + yield* logUpdaterInfo("ignoring update outside the selected channel or its floor", { version: info.version, channel: state.channel, }); @@ -986,6 +1016,20 @@ export const make = Effect.gen(function* () { Effect.flatMap( Effect.fn("desktop.updates.applyUpdateDownloaded")(function* (info) { const state = yield* Ref.get(updateStateRef); + // A download can finish after its track switch moved on; never offer that build. + if (!(yield* offerable(info.version, state))) { + yield* logUpdaterInfo("ignoring download outside the selected channel or its floor", { + version: info.version, + channel: state.channel, + }); + // Its progress events left the updater "downloading", which would block every check. + if (state.status === "downloading") { + yield* setState( + reduceDesktopUpdateStateOnNoUpdate(state, yield* currentIsoTimestamp), + ); + } + return; + } yield* setState(reduceDesktopUpdateStateOnDownloadComplete(state, info.version)); yield* logUpdaterInfo("update downloaded", { version: info.version }); }), @@ -1092,9 +1136,17 @@ export const make = Effect.gen(function* () { }).pipe(Effect.withSpan("desktop.updates.configure")), setChannel: Effect.fn("desktop.updates.setChannel")(function* ( nextChannel: DesktopUpdateChannel, + options?: { readonly minimumVersion?: string | undefined }, ) { yield* Effect.annotateCurrentSpan({ channel: nextChannel }); - const activeAction = yield* tryStartChannelChange; + // A background check holds the updater only briefly, so wait it out instead of refusing. + const activeAction = yield* tryStartChannelChange.pipe( + Effect.repeat({ + while: (active) => Option.isSome(active) && active.value === "check", + schedule: Schedule.spaced(CHANNEL_CHANGE_RETRY_DELAY), + times: CHANNEL_CHANGE_RETRIES, + }), + ); if (Option.isSome(activeAction)) { return yield* new DesktopUpdateActionInProgressError({ action: activeAction.value === "install-recovery" ? "install" : activeAction.value, @@ -1103,8 +1155,16 @@ export const make = Effect.gen(function* () { } return yield* Effect.gen(function* () { + yield* Ref.set(releaseFloorRef, Option.fromNullishOr(options?.minimumVersion)); const state = yield* Ref.get(updateStateRef); - if (nextChannel === state.channel) { + const retained = state.downloadedVersion ?? state.availableVersion; + // A build from another track re-checks, so retrying a track switch finds an older release. + // So does a retained offer below the new floor, which must not install. + if ( + nextChannel === state.channel && + nextChannel === desktopUpdateChannelOf(environment.appVersion) && + (retained === null || (yield* offerable(retained, state))) + ) { return state; } @@ -1124,15 +1184,19 @@ export const make = Effect.gen(function* () { } yield* applyAutoUpdaterChannel(nextChannel); - const allowDowngrade = yield* electronUpdater.allowDowngrade; - yield* electronUpdater.setAllowDowngrade(true); - yield* checkForUpdates("channel-change", "held").pipe( - Effect.ensuring(electronUpdater.setAllowDowngrade(allowDowngrade).pipe(Effect.ignore)), + // Only leaving the running build's track can need an older release. + yield* checkForUpdates( + "channel-change", + "held", + nextChannel !== desktopUpdateChannelOf(environment.appVersion), ); return yield* Ref.get(updateStateRef); }).pipe(Effect.ensuring(finishUpdateAction("channel"))); }), - check: Effect.fn("desktop.updates.check")(function* (reason: string) { + check: Effect.fn("desktop.updates.check")(function* ( + reason: string, + options?: { readonly allowDowngrade?: boolean }, + ) { yield* Effect.annotateCurrentSpan({ reason }); if (!(yield* Ref.get(updaterConfiguredRef))) { return { @@ -1140,7 +1204,7 @@ export const make = Effect.gen(function* () { state: yield* Ref.get(updateStateRef), }; } - const checked = yield* checkForUpdates(reason); + const checked = yield* checkForUpdates(reason, "acquire", options?.allowDowngrade === true); return { checked, state: yield* Ref.get(updateStateRef), diff --git a/apps/desktop/src/updates/releaseNotes.ts b/apps/desktop/src/updates/releaseNotes.ts index 69d710ff7b269..8698d8d499118 100644 --- a/apps/desktop/src/updates/releaseNotes.ts +++ b/apps/desktop/src/updates/releaseNotes.ts @@ -1,6 +1,6 @@ import type { DesktopUpdateChannel, DesktopUpdateReleaseNote } from "@t3tools/contracts"; -import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts"; +import { desktopUpdateChannelOf } from "@t3tools/shared/cliRelease"; interface ElectronReleaseNoteInfo { readonly version: string; @@ -134,7 +134,7 @@ export function normalizeDesktopUpdateReleaseNotes( : Array.isArray(releaseNotes) ? releaseNotes.filter(isElectronReleaseNoteInfo) : [] - ).filter((entry) => resolveDefaultDesktopUpdateChannel(entry.version) === channel); + ).filter((entry) => desktopUpdateChannelOf(entry.version) === channel); const normalizedNotes = rawNotes.flatMap((entry) => { const { items, totalItems } = extractReleaseNoteItems(entry.note); diff --git a/apps/desktop/src/updates/updateChannels.ts b/apps/desktop/src/updates/updateChannels.ts index d79a374a668a5..b07735ceb7a29 100644 --- a/apps/desktop/src/updates/updateChannels.ts +++ b/apps/desktop/src/updates/updateChannels.ts @@ -1,13 +1,6 @@ -import type { DesktopUpdateChannel } from "@t3tools/contracts"; - -const NIGHTLY_VERSION_PATTERN = /^[^-+]+-nightly\.\d{8}\.\d+$/; // Preview builds share nightly branding but ship without an update feed. const PRERELEASE_VERSION_PATTERN = /^[^-+]+-(?:nightly|preview)\.\d{8}\.\d+$/; export function isNightlyDesktopVersion(version: string): boolean { return PRERELEASE_VERSION_PATTERN.test(version); } - -export function resolveDefaultDesktopUpdateChannel(appVersion: string): DesktopUpdateChannel { - return NIGHTLY_VERSION_PATTERN.test(appVersion) ? "nightly" : "latest"; -} diff --git a/apps/desktop/src/updates/updatesTestHarness.ts b/apps/desktop/src/updates/updatesTestHarness.ts index 1d8c21a42dfe5..d8d16b7dbfd62 100644 --- a/apps/desktop/src/updates/updatesTestHarness.ts +++ b/apps/desktop/src/updates/updatesTestHarness.ts @@ -35,6 +35,11 @@ export interface UpdatesHarnessOptions { readonly platform?: NodeJS.Platform; readonly packageType?: string | undefined; readonly installRecord?: string | undefined; + readonly appVersion?: string; + /** Starts from in-memory settings with this track saved, as after a restart. */ + readonly savedChannel?: DesktopAppSettings.DesktopSettings["updateChannel"]; + /** Adds a WSL backend beside the primary, as on a Windows host with WSL enabled. */ + readonly wslBackend?: boolean; } export function makeHarness(options: UpdatesHarnessOptions = {}) { @@ -42,6 +47,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { let quitAndInstallCount = 0; let downloadCount = 0; let allowDowngrade = false; + const checkAllowedDowngrade: boolean[] = []; let fullChangelog = false; const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = []; const listeners = new Map void>>(); @@ -86,6 +92,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { setDisableDifferentialDownload: () => options.setDisableDifferentialDownload ?? Effect.void, checkForUpdates: Effect.sync(() => { checkCount += 1; + checkAllowedDowngrade.push(allowDowngrade); }).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)), downloadUpdate: Effect.sync(() => { downloadCount += 1; @@ -149,7 +156,17 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { waitForReady: () => Effect.succeed(true), }; const layerBackend = DesktopBackendPool.layerTest( - options.backendInstances ?? [stubBackendInstance], + options.backendInstances ?? + (options.wslBackend + ? [ + stubBackendInstance, + { + ...stubBackendInstance, + id: DesktopBackendPool.BackendInstanceId("wsl:Ubuntu"), + label: Effect.succeed("Ubuntu"), + }, + ] + : [stubBackendInstance]), ); const layerEnvironment = DesktopEnvironment.layer({ @@ -157,7 +174,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { homeDirectory: `/tmp/t3-desktop-updates-home-${process.pid}`, platform: options.platform ?? "darwin", processArch: "x64", - appVersion: "1.2.3", + appVersion: options.appVersion ?? "1.2.3", appPath: "/repo", isPackaged: true, resourcesPath: "/missing/resources", @@ -178,10 +195,13 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { let testSettings: DesktopAppSettings.DesktopSettings = { ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, + ...(options.savedChannel === undefined + ? {} + : { updateChannel: options.savedChannel, updateChannelConfiguredByUser: true }), }; const setUpdateChannelError = options.setUpdateChannelError; const layerSettings = - setUpdateChannelError || options.beforeSetUpdateChannel + setUpdateChannelError || options.beforeSetUpdateChannel || options.savedChannel ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.sync(() => testSettings), load: Effect.sync(() => testSettings), @@ -279,6 +299,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) { downloadCount: () => downloadCount, feedUrls: (): ElectronUpdater.ElectronUpdaterFeedUrl[] => feedUrls, fullChangelog: () => fullChangelog, + /** Whether each feed check could offer an older release, and the flag now. */ + checkAllowedDowngrade, + allowDowngrade: () => allowDowngrade, listenerCount: () => Array.from(listeners.values()).reduce( (total, eventListeners) => total + eventListeners.size, diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index 84877d41a70c9..1e3157adfff59 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -313,3 +313,36 @@ it.layer(NodeServices.layer)("pruning runtimes on start", (it) => { }), ); }); + +it("a host crosses tracks only to a release that knows its whole database", () => { + const cross = ( + serverVersion: string, + targetVersion: string, + targetDatabaseMigration: number | undefined, + databaseMigration = 72, + ) => + ServerSelfUpdate.trackSwitchBlockReason({ + serverVersion, + targetVersion, + targetDatabaseMigration, + databaseMigration: () => databaseMigration, + }); + const nightly = "0.1.4-nightly.20261027.3796"; + + expect(cross(nightly, "0.1.4", 72)).toBeNull(); + // Another client moved this host to a newer Nightly after the switch was planned. + expect(cross(nightly, "0.1.4", 72, 73)).toContain("does not support yet"); + expect(cross(nightly, "0.1.4", undefined)).toContain("Cannot confirm"); + // A Stable tag can carry a migration the latest Nightly lacks. + expect(cross("0.1.4", nightly, 72, 73)).toContain("does not support yet"); + expect(cross("0.1.3", "0.1.4", undefined)).toBeNull(); + // A database that exists but cannot be read proves nothing. + expect( + ServerSelfUpdate.trackSwitchBlockReason({ + serverVersion: nightly, + targetVersion: "0.1.4", + targetDatabaseMigration: 72, + databaseMigration: () => undefined, + }), + ).toContain("Cannot confirm"); +}); diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index ab6a69fc50a83..d72c5e9218df2 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -20,11 +20,12 @@ import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; import { HttpClient } from "effect/http"; -import { CLI_RELEASE_BASE_URL_ENV } from "@t3tools/shared/cliRelease"; +import { CLI_RELEASE_BASE_URL_ENV, crossesReleaseTrack } from "@t3tools/shared/cliRelease"; import packageJson from "../../package.json" with { type: "json" }; import * as ServerConfig from "../config.ts"; import * as DesktopAppUpdate from "../desktopUpdate/DesktopAppUpdate.ts"; +import { databaseMigrationHead } from "../persistence/databaseMigrationHead.ts"; import * as ProcessRunner from "../processRunner.ts"; import { ensurePinnedRuntimeInstalled, @@ -93,6 +94,26 @@ export const pruneRuntimesOnStart = Effect.fn("cloud.server_self_update.prune_on }, ); +/** The client checks this when planning; the host checks again because its database may have + * moved on since (another client updated it). Stable and Nightly each build their own commits, + * so crossing tracks needs the target release to know every migration this database holds. */ +export function trackSwitchBlockReason(input: { + readonly serverVersion: string; + readonly targetVersion: string; + readonly targetDatabaseMigration: number | undefined; + /** Read only for a crossing; undefined when the database cannot be read. */ + readonly databaseMigration: () => number | undefined; +}): string | null { + if (!crossesReleaseTrack(input.serverVersion, input.targetVersion)) return null; + const databaseMigration = input.databaseMigration(); + if (input.targetDatabaseMigration === undefined || databaseMigration === undefined) { + return `Cannot confirm this database works with t3@${input.targetVersion}. Switch tracks from Settings → About → Update track, which checks it first.`; + } + return input.targetDatabaseMigration < databaseMigration + ? `This database has changes t3@${input.targetVersion} does not support yet; it stays on this release.` + : null; +} + export const make = Effect.fn("cloud.server_self_update.make")(function* () { const serverConfig = yield* ServerConfig.ServerConfig; const desktopAppUpdate = yield* DesktopAppUpdate.DesktopAppUpdate; @@ -119,12 +140,27 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { ? new ServerSelfUpdateError({ reason }) : new ServerSelfUpdateError({ reason, cause }); + const crossesTrackUnsafely = (input: ServerSelfUpdateInput) => + trackSwitchBlockReason({ + serverVersion: packageJson.version, + targetVersion: input.targetVersion, + targetDatabaseMigration: input.targetDatabaseMigration, + databaseMigration: () => databaseMigrationHead(serverConfig.dbPath), + }); + const update: ServerSelfUpdate["Service"]["update"] = Effect.fn( "cloud.server_self_update.update", )(function* (input, reportProgress = () => Effect.void, onHandoffAccepted = () => Effect.void) { if (capability === "desktop-managed") { + const unsafeCrossing = input.channel === undefined ? null : crossesTrackUnsafely(input); + if (unsafeCrossing !== null) return yield* failWith(unsafeCrossing); if (desktopAppUpdate.available) { - return yield* desktopAppUpdate.run(reportProgress); + return yield* desktopAppUpdate.run( + reportProgress, + input.channel === undefined + ? undefined + : { channel: input.channel, targetVersion: input.targetVersion }, + ); } return yield* failWith( "This server is managed by the T3 Code desktop app on its machine; update the desktop app to update it.", @@ -136,6 +172,8 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () { ); } + const unsafeCrossing = crossesTrackUnsafely(input); + if (unsafeCrossing !== null) return yield* failWith(unsafeCrossing); const targetVersion = input.targetVersion.trim(); if (!isExactServiceVersion(targetVersion)) { return yield* failWith(`'${targetVersion}' is not an exact t3 version.`); diff --git a/apps/server/src/cloud/servicePreflight.test.ts b/apps/server/src/cloud/servicePreflight.test.ts index 18a5323cd590c..efc37266be8b0 100644 --- a/apps/server/src/cloud/servicePreflight.test.ts +++ b/apps/server/src/cloud/servicePreflight.test.ts @@ -1,4 +1,16 @@ +import * as NodeSqlite from "node:sqlite"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; + +import { + BUILD_MIGRATION_HEAD, + databaseMigrationHead, +} from "../persistence/databaseMigrationHead.ts"; +import { migrationManifest } from "../persistence/Migrations.ts"; import { runServicePreflight } from "./servicePreflight.ts"; import { SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts"; @@ -20,3 +32,49 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la expect(reason).toContain("t3 --version"); expect(reason).toContain("which -a t3"); }); + +it.layer(NodeServices.layer)("service preflight database", (it) => { + it.effect("blocks a database that a newer build migrated", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const databasePath = path.join( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-preflight-" }), + "state.sqlite", + ); + const db = new NodeSqlite.DatabaseSync(databasePath); + db.exec("CREATE TABLE effect_sql_migrations (migration_id INTEGER PRIMARY KEY, name TEXT)"); + const insert = db.prepare("INSERT INTO effect_sql_migrations VALUES (?, ?)"); + for (const [id, name] of migrationManifest) insert.run(id, name); + const preflight = () => + runServicePreflight({ databasePath, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL }); + + expect(preflight().status).toBe("ready"); + expect(databaseMigrationHead(databasePath)).toBe(BUILD_MIGRATION_HEAD); + + const newerId = Math.max(...migrationManifest.map(([id]) => id)) + 1; + insert.run(newerId, "FromNightly"); + db.close(); + const result = preflight(); + expect(result.status).toBe("blocked"); + expect(result.status === "blocked" ? result.reason : "").toContain(`${newerId}_FromNightly`); + // Servers report this head, so clients see a database a newer build migrated. + expect(databaseMigrationHead(databasePath)).toBe(newerId); + + // A database that exists but cannot be read has an unknown history, so it blocks. + const unreadable = path.join(path.dirname(databasePath), "unreadable.sqlite"); + yield* fs.writeFileString(unreadable, "not a database"); + expect(databaseMigrationHead(unreadable)).toBeUndefined(); + expect( + runServicePreflight({ + databasePath: unreadable, + launcherProtocol: SERVICE_LAUNCHER_PROTOCOL, + }).status, + ).toBe("blocked"); + // A missing database is a fresh install. + expect(databaseMigrationHead(path.join(path.dirname(databasePath), "missing.sqlite"))).toBe( + BUILD_MIGRATION_HEAD, + ); + }), + ); +}); diff --git a/apps/server/src/cloud/servicePreflight.ts b/apps/server/src/cloud/servicePreflight.ts index 8865ce7b3d11f..9479f3f4a8194 100644 --- a/apps/server/src/cloud/servicePreflight.ts +++ b/apps/server/src/cloud/servicePreflight.ts @@ -1,4 +1,5 @@ import packageJson from "../../package.json" with { type: "json" }; +import { BUILD_MIGRATION_HEAD, recordedMigrations } from "../persistence/databaseMigrationHead.ts"; import { SERVICE_LAUNCHER_PROTOCOL } from "./serviceProtocol.ts"; export type ServicePreflightResult = @@ -34,9 +35,34 @@ export function runServicePreflight(input: { }; } + const newer = newerRecordedMigrations(input.databasePath); + if (newer === null) { + return { + status: "blocked", + version, + reason: `Could not read this database to confirm t3@${version} supports it.`, + }; + } + if (newer.length > 0) { + return { + status: "blocked", + version, + reason: `This database has migrations that t3@${version} does not know: ${newer.join(", ")}. A newer build wrote them, and running t3@${version} on this database is not safe. Stay on the newer release, or wait for a release that includes these migrations.`, + }; + } + return { status: "ready", version, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL }; } +/** Migrations a newer build recorded, or null when the database exists but cannot be read. + * Startup already handles renamed ids this build knows. */ +function newerRecordedMigrations(databasePath: string): ReadonlyArray | null { + const recorded = recordedMigrations(databasePath); + return recorded === null + ? null + : recorded.filter(({ id }) => id > BUILD_MIGRATION_HEAD).map(({ id, name }) => `${id}_${name}`); +} + export function decodeServicePreflightResult(value: unknown): ServicePreflightResult | undefined { if (typeof value !== "object" || value === null) { return undefined; diff --git a/apps/server/src/desktopUpdate/DesktopAppUpdate.test.ts b/apps/server/src/desktopUpdate/DesktopAppUpdate.test.ts index 86d445d1a52c4..f90bef39fcf03 100644 --- a/apps/server/src/desktopUpdate/DesktopAppUpdate.test.ts +++ b/apps/server/src/desktopUpdate/DesktopAppUpdate.test.ts @@ -132,6 +132,24 @@ it.layer(NodeServices.layer)("desktop app update", (it) => { }), ); + it.effect("succeeds without an install when a track switch finds the host current", () => + Effect.gen(function* () { + const { service } = yield* makeHarness({ + reports: (requestId) => [ + report(requestId, makeState({ status: "up-to-date", currentVersion: "1.2.3" }), { + outcome: "up-to-date", + }), + ], + }); + const trackSwitch = { channel: "latest" as const, targetVersion: "1.2.3" }; + + expect(yield* service.run(() => Effect.void, trackSwitch)).toEqual({ + targetVersion: "1.2.3", + method: "desktop-app", + }); + }), + ); + it.effect("replays a retained commit failure for the preparation token", () => Effect.gen(function* () { const { service } = yield* makeHarness({ diff --git a/apps/server/src/desktopUpdate/DesktopAppUpdate.ts b/apps/server/src/desktopUpdate/DesktopAppUpdate.ts index d02c3108777dd..e901b04cfb5d7 100644 --- a/apps/server/src/desktopUpdate/DesktopAppUpdate.ts +++ b/apps/server/src/desktopUpdate/DesktopAppUpdate.ts @@ -44,6 +44,7 @@ export class DesktopAppUpdate extends Context.Service< reportProgress: ( stage: ServerSelfUpdateProgressStage, ) => Effect.Effect, + trackSwitch?: DesktopTelemetryReceiver.DesktopTrackSwitch, ) => Effect.Effect; /** Success stops the server; returns only on installation failure or timeout. */ readonly commit: ( @@ -71,6 +72,7 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* ( reportProgress: ( stage: ServerSelfUpdateProgressStage, ) => Effect.Effect, + trackSwitch: boolean, ) { const lastStage = yield* Ref.make(null); const emitStage = ( @@ -122,6 +124,11 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* ( return { targetVersion, method: "desktop-app" as const, desktopUpdateToken: requestId }; } if (report.outcome === "up-to-date") { + // A switch's host already on the newest build of its track only saved the track. + if (trackSwitch) { + yield* Ref.set(inFlight, false); + return { targetVersion: report.state.currentVersion, method: "desktop-app" as const }; + } return yield* failWith( `The T3 Code desktop app on this machine is already up to date on ${report.state.currentVersion}.`, ); @@ -132,7 +139,7 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* ( }); const run: DesktopAppUpdate["Service"]["run"] = Effect.fn("desktopUpdate.desktopAppUpdate.run")( - function* (reportProgress) { + function* (reportProgress, trackSwitch) { if (!available) { return yield* failWith( "This server was not started by the T3 Code desktop app, so it cannot drive a desktop update.", @@ -154,13 +161,18 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* ( // Subscribe before requesting to catch fast reports. const { changes } = yield* receiver.desktopUpdates; yield* receiver - .requestDesktopUpdate(requestId) + .requestDesktopUpdate(requestId, trackSwitch) .pipe( Effect.mapError((error) => failWith("Could not reach the T3 Code desktop app on this machine.", error), ), ); - return yield* consumeReports(requestId, changes, reportProgress).pipe( + return yield* consumeReports( + requestId, + changes, + reportProgress, + trackSwitch !== undefined, + ).pipe( Effect.onInterrupt(() => receiver.cancelDesktopUpdate(requestId).pipe(Effect.ignore)), ); }), diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 90ae4519bed92..cef18fbe325c3 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -20,6 +20,7 @@ import { readAgentActivityPublishingActive } from "../cloud/config.ts"; import { resolveServerSelfUpdateCapability } from "../cloud/selfUpdate.ts"; import { resolveServiceLauncherMode } from "../cloud/serviceLauncherClient.ts"; import * as ServerConfig from "../config.ts"; +import { databaseMigrationHead } from "../persistence/databaseMigrationHead.ts"; import * as ProcessRunner from "../processRunner.ts"; import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts"; import { detectServerEnvironmentMachineKind } from "./ServerEnvironmentMachine.ts"; @@ -203,6 +204,7 @@ export const make = Effect.gen(function* () { const desktopAppUpdate = serverSelfUpdate === "desktop-managed" && serverConfig.desktopTelemetryControlFd !== undefined; + const databaseMigration = databaseMigrationHead(serverConfig.dbPath); const descriptor: ExecutionEnvironmentDescriptor = { environmentId, label, @@ -213,6 +215,7 @@ export const make = Effect.gen(function* () { }, serverVersion: packageJson.version, orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION, + ...(databaseMigration === undefined ? {} : { databaseMigration }), capabilities: { repositoryIdentity: true, connectionProbe: true, @@ -266,7 +269,7 @@ export const make = Effect.gen(function* () { ...(serverSelfUpdate === "boot-service" || desktopAppUpdate ? { serverSelfUpdateProgress: true } : {}), - ...(desktopAppUpdate ? { desktopAppUpdate: true } : {}), + ...(desktopAppUpdate ? { desktopAppUpdate: true, desktopAppChannelSwitch: true } : {}), serverBrowser: true, peerLinks: true, mcpModeLimitHeader: true, diff --git a/apps/server/src/persistence/Migrations.test.ts b/apps/server/src/persistence/Migrations.test.ts index 67f238b7484f5..54e4a711aa8a1 100644 --- a/apps/server/src/persistence/Migrations.test.ts +++ b/apps/server/src/persistence/Migrations.test.ts @@ -1,7 +1,10 @@ import { assert, describe, it } from "@effect/vitest"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; +import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; import * as SqlClient from "effect/sql/SqlClient"; import { migrationManifest, runMigrations } from "./Migrations.ts"; @@ -123,3 +126,20 @@ describe("runMigrations", () => { }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))), ); }); + +// The npm launcher reads the highest migration id from these file names. +it.effect("registers every migration file", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = path.join(import.meta.dirname, "Migrations"); + const files = (yield* fs.readDirectory(dir)).flatMap((name) => { + const match = /^(\d+)_(\w+)\.ts$/.exec(name); + return match ? [[Number(match[1]), match[2]] as const] : []; + }); + assert.deepStrictEqual( + files.toSorted(([a], [b]) => a - b), + migrationManifest, + ); + }).pipe(Effect.provide(NodeServices.layer)), +); diff --git a/apps/server/src/persistence/databaseMigrationHead.ts b/apps/server/src/persistence/databaseMigrationHead.ts new file mode 100644 index 0000000000000..046039a703410 --- /dev/null +++ b/apps/server/src/persistence/databaseMigrationHead.ts @@ -0,0 +1,49 @@ +// @effect-diagnostics nodeBuiltinImport:off - read synchronously by the preflight subprocess and at startup. +import * as NodeFS from "node:fs"; +import * as NodeSqlite from "node:sqlite"; + +import { migrationManifest } from "./Migrations.ts"; + +export const BUILD_MIGRATION_HEAD = Math.max(...migrationManifest.map(([id]) => id)); + +/** Migrations the database records, read-only. A missing or new database records none; one that + * exists but cannot be read is null, since its history is unknown. */ +export function recordedMigrations( + databasePath: string, +): ReadonlyArray<{ readonly id: number; readonly name: string }> | null { + let db: NodeSqlite.DatabaseSync; + try { + db = new NodeSqlite.DatabaseSync(databasePath, { readOnly: true }); + } catch { + return NodeFS.existsSync(databasePath) ? null : []; + } + try { + const hasTable = db + .prepare( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'effect_sql_migrations'", + ) + .get(); + if (hasTable === undefined) return []; + return db + .prepare("SELECT migration_id, name FROM effect_sql_migrations ORDER BY migration_id") + .all() + .map((row) => ({ id: Number(row.migration_id), name: String(row.name) })); + } catch { + return null; + } finally { + try { + db.close(); + } catch { + // A read-only handle has nothing to flush; the result above already stands. + } + } +} + +/** Highest migration the database holds once this build's startup migrations have run. A newer + * build may have recorded more than this build knows. Undefined when the database is unreadable. */ +export function databaseMigrationHead(databasePath: string): number | undefined { + const recorded = recordedMigrations(databasePath); + return recorded === null + ? undefined + : Math.max(BUILD_MIGRATION_HEAD, ...recorded.map(({ id }) => id)); +} diff --git a/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts b/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts index 770359b3e9307..70dc2e8d750ef 100644 --- a/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts +++ b/apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts @@ -8,6 +8,7 @@ import { type DesktopHostTelemetryMessage as DesktopHostTelemetryMessageValue, type DesktopHostTelemetrySnapshot, DesktopTelemetryControlMessage, + type DesktopUpdateChannel, type DesktopUpdateStatusReport, type ResourceTelemetrySourceStatus, } from "@t3tools/contracts"; @@ -149,6 +150,12 @@ export interface DesktopTelemetryReceiverHealth { readonly lastError: Option.Option; } +/** Moves the desktop app to `channel` and installs only `targetVersion`. */ +export interface DesktopTrackSwitch { + readonly channel: DesktopUpdateChannel; + readonly targetVersion: string; +} + export class DesktopTelemetryReceiver extends Context.Service< DesktopTelemetryReceiver, { @@ -176,6 +183,7 @@ export class DesktopTelemetryReceiver extends Context.Service< ) => Effect.Effect; readonly requestDesktopUpdate: ( requestId: string, + trackSwitch?: DesktopTrackSwitch, ) => Effect.Effect; readonly commitDesktopUpdate: ( requestId: string, @@ -633,11 +641,12 @@ export const make = Effect.fn("resourceTelemetry.desktopTelemetryReceiver.make") subscribeHealth: subscribeBeforeSnapshotWithoutMutex(healthChanges, Ref.get(health)), setDiagnosticsDemand, requestDesktopUpdate: Effect.fn("DesktopTelemetryReceiver.requestDesktopUpdate")( - function (requestId) { + function (requestId, trackSwitch) { return sendControlMessage({ version: 1, type: "requestDesktopUpdate", requestId, + ...trackSwitch, }); }, ), diff --git a/apps/web/src/components/ServerUpdateAction.browser.test.tsx b/apps/web/src/components/ServerUpdateAction.browser.test.tsx index 5f793b3ca90e4..ed3a723652a56 100644 --- a/apps/web/src/components/ServerUpdateAction.browser.test.tsx +++ b/apps/web/src/components/ServerUpdateAction.browser.test.tsx @@ -14,6 +14,7 @@ const testState = vi.hoisted(() => ({ toast: vi.fn(), clipboard: vi.fn(), sessionAtom: null as unknown, + serverVersions: new Map(), })); vi.mock(import("~/hooks/useCopyToClipboard"), async (original) => ({ @@ -36,6 +37,16 @@ vi.mock(import("~/state/session"), async (original) => { }, }; }); +vi.mock(import("~/state/entities"), async (original) => ({ + ...(await original()), + useServerConfigs: (() => + new Map( + [...testState.serverVersions].map(([id, serverVersion]) => [ + id, + { environment: { serverVersion } }, + ]), + )) as never, +})); vi.mock(import("~/state/use-atom-command"), async (original) => ({ ...(await original()), useAtomCommand: (() => testState.updateServer) as never, @@ -84,6 +95,7 @@ function setup(): Disposable { testState.updateServer.mockReset(); testState.toast.mockReset(); testState.clipboard.mockReset(); + testState.serverVersions.clear(); sessionAtom = Atom.make(AsyncResult.success(currentSession)); testState.sessionAtom = sessionAtom; resetConfirmDialogForTests(); @@ -212,4 +224,17 @@ describe("ServerUpdatesAction", () => { ); expect(button(screen, "Update all")!.disabled).toBe(false); }); + + it("skips a machine whose update would cross Stable and Nightly", async () => { + using _state = setup(); + testState.serverVersions.set("batch-a", "0.0.30-nightly.20261001.1"); + testState.updateServer.mockResolvedValue(success()); + await using screen = await mount(); + await click(screen, "Update all"); + + expect(testState.updateServer).toHaveBeenCalledTimes(1); + expect(testState.toast).toHaveBeenCalledWith( + expect.objectContaining({ type: "success", title: "Office updated" }), + ); + }); }); diff --git a/apps/web/src/components/ServerUpdateAction.tsx b/apps/web/src/components/ServerUpdateAction.tsx index 3f7675cd96631..141f139aefd7b 100644 --- a/apps/web/src/components/ServerUpdateAction.tsx +++ b/apps/web/src/components/ServerUpdateAction.tsx @@ -7,6 +7,7 @@ import { import type { AsyncResult } from "effect/reactivity"; import { environmentSession } from "~/state/session"; import type { + DesktopUpdateChannel, EnvironmentId, ServerInstallation, ServerSelfUpdateCapability, @@ -26,6 +27,8 @@ import { serverEnvironment, updateOutdatedServer } from "~/state/server"; import { appAtomRegistry } from "~/rpc/atomRegistry"; import { useAtomCommand } from "~/state/use-atom-command"; import { manualServerUpdateCommand } from "~/versionSkew"; +import { useServerConfigs } from "~/state/entities"; +import { crossesReleaseTrack } from "@t3tools/shared/cliRelease"; import { Button } from "./ui/button"; import { toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; @@ -41,6 +44,29 @@ export function serverUpdateStageLabel(stage: ServerUpdateStage): string { return UPDATE_STAGE_LABELS[stage]; } +const CROSS_TRACK_NOTE = "Switch tracks in About → Update track"; + +/** Hosts refuse a Stable/Nightly crossing outside a track switch, which checks the database first. */ +function refusedCrossing( + target: { + readonly selfUpdate: ServerSelfUpdateCapability | null; + readonly targetVersion: string; + readonly channel?: DesktopUpdateChannel | undefined; + }, + serverVersion: string | undefined, +): boolean { + return ( + target.channel === undefined && + target.selfUpdate !== null && + serverVersion !== undefined && + crossesReleaseTrack(serverVersion, target.targetVersion) + ); +} + +function CrossTrackNote() { + return {CROSS_TRACK_NOTE}; +} + function updateFailureMessage(error: unknown): string { return error instanceof Error ? error.message : "Server update failed."; } @@ -52,6 +78,12 @@ export interface ServerUpdateTarget { readonly installation?: ServerInstallation | undefined; readonly desktopAppUpdate?: boolean; readonly targetVersion: string; + /** Set only by a track switch; the host's desktop app moves to this track. */ + readonly channel?: DesktopUpdateChannel; + /** A track switch's checked release migration; the host re-checks its own database. */ + readonly targetDatabaseMigration?: number; + /** The host already runs its track's newest build and only saves the track. */ + readonly trackOnly?: boolean; } type UpdateButtonProps = Pick, "variant" | "size" | "className"> & { @@ -79,7 +111,13 @@ export function useServerUpdate() { pendingUpdateEnvironmentIds.add(environmentId); const result = await updateServer({ environmentId, - input: { targetVersion }, + input: { + targetVersion, + ...(target.channel === undefined ? {} : { channel: target.channel }), + ...(target.targetDatabaseMigration === undefined + ? {} + : { targetDatabaseMigration: target.targetDatabaseMigration }), + }, }); pendingUpdateEnvironmentIds.delete(environmentId); if (result._tag === "Failure") { @@ -118,12 +156,14 @@ export function ServerUpdatesAction({ readonly targets: ReadonlyArray; }) { const update = useServerUpdate(); + const serverConfigs = useServerConfigs(); const pending = useRef(false); const [isPending, setIsPending] = useState(false); const eligible = targets.filter( (target) => target.selfUpdate !== null && - (target.selfUpdate !== "desktop-managed" || target.desktopAppUpdate), + (target.selfUpdate !== "desktop-managed" || target.desktopAppUpdate) && + !refusedCrossing(target, serverConfigs.get(target.environmentId)?.environment.serverVersion), ); const handleUpdate = async () => { if (pending.current) return; @@ -198,6 +238,7 @@ export function ServerUpdateAction({ installation, desktopAppUpdate = false, targetVersion, + channel, label = "Update", variant = "outline", size = "xs", @@ -205,6 +246,7 @@ export function ServerUpdateAction({ appearance = "button", }: ServerUpdateTarget & UpdateButtonProps) { const isDesktopAppUpdate = selfUpdate === "desktop-managed"; + const serverVersion = useServerConfigs().get(environmentId)?.environment.serverVersion; const sessionStateAtom = environmentSession.sessionStateAtom(environmentId); const canUpdate = canUpdateServer(useAtomValue(sessionStateAtom)); const update = useServerUpdate(); @@ -264,6 +306,10 @@ export function ServerUpdateAction({ ); } + if (refusedCrossing({ selfUpdate, targetVersion, channel }, serverVersion)) { + return ; + } + const manualCommand = selfUpdate === null ? manualServerUpdateCommand(targetVersion, installation) : null; const actionLabel = @@ -351,6 +397,9 @@ export function OutdatedServerUpdateAction({ description: `Reconnected on t3@${result.value.targetVersion}.`, }); }; + if (fromVersion !== undefined && crossesReleaseTrack(fromVersion, targetVersion)) { + return ; + } return ( + + ) : ( + "Switches this app and every connected machine whose database the target release supports." + ) + } control={