From 5ed175ecc396c9810cce54d2913d0f3c8f85e9ca Mon Sep 17 00:00:00 2001 From: BinBandit Date: Mon, 9 Mar 2026 19:47:39 +1100 Subject: [PATCH 1/3] fix(web): avoid push and PR defaults without origin --- apps/server/src/git/Layers/GitCore.test.ts | 3 + apps/server/src/git/Layers/GitCore.ts | 4 +- apps/server/src/wsServer.test.ts | 3 +- apps/web/src/components/ChatView.browser.tsx | 1 + .../GitActionsControl.logic.test.ts | 63 +++++++++++++++++++ .../src/components/GitActionsControl.logic.ts | 28 ++++++++- apps/web/src/components/GitActionsControl.tsx | 17 +++-- packages/contracts/src/git.ts | 1 + 8 files changed, 111 insertions(+), 9 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index d03ad60615f..88327327332 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -247,6 +247,7 @@ it.layer(TestLayer)("git integration", (it) => { yield* initRepoWithCommit(tmp); const result = yield* listGitBranches({ cwd: tmp }); expect(result.isRepo).toBe(true); + expect(result.hasOriginRemote).toBe(false); expect(result.branches.length).toBeGreaterThanOrEqual(1); }), ); @@ -260,6 +261,7 @@ it.layer(TestLayer)("git integration", (it) => { const tmp = yield* makeTmpDir(); const result = yield* listGitBranches({ cwd: tmp }); expect(result.isRepo).toBe(false); + expect(result.hasOriginRemote).toBe(false); expect(result.branches).toEqual([]); }), ); @@ -418,6 +420,7 @@ it.layer(TestLayer)("git integration", (it) => { const result = yield* listGitBranches({ cwd: tmp }); const firstRemoteIndex = result.branches.findIndex((branch) => branch.isRemote); + expect(result.hasOriginRemote).toBe(true); expect(firstRemoteIndex).toBeGreaterThan(0); expect(result.branches.slice(0, firstRemoteIndex).every((branch) => !branch.isRemote)).toBe( true, diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index a288b2f3799..94a89504309 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -844,7 +844,7 @@ const makeGitCore = Effect.gen(function* () { if (localBranchResult.code !== 0) { const stderr = localBranchResult.stderr.trim(); if (stderr.toLowerCase().includes("not a git repository")) { - return { branches: [], isRepo: false }; + return { branches: [], isRepo: false, hasOriginRemote: false }; } return yield* createGitCommandError( "GitCore.listBranches", @@ -1010,7 +1010,7 @@ const makeGitCore = Effect.gen(function* () { const branches = [...localBranches, ...remoteBranches]; - return { branches, isRepo: true }; + return { branches, isRepo: true, hasOriginRemote: remoteNames.includes("origin") }; }); const createWorktree: GitCoreShape["createWorktree"] = (input) => diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 80d57d7c6f9..764e455f550 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -1577,6 +1577,7 @@ describe("WebSocket Server", () => { Effect.succeed({ branches: [], isRepo: false, + hasOriginRemote: false, }), ); const initRepo = vi.fn(() => Effect.void); @@ -1608,7 +1609,7 @@ describe("WebSocket Server", () => { const listResponse = await sendRequest(ws, WS_METHODS.gitListBranches, { cwd: "/repo/path" }); expect(listResponse.error).toBeUndefined(); - expect(listResponse.result).toEqual({ branches: [], isRepo: false }); + expect(listResponse.result).toEqual({ branches: [], isRepo: false, hasOriginRemote: false }); expect(listBranches).toHaveBeenCalledWith({ cwd: "/repo/path" }); const initResponse = await sendRequest(ws, WS_METHODS.gitInit, { cwd: "/repo/path" }); diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index e2fd573fe8c..23abfffc154 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -266,6 +266,7 @@ function resolveWsRpc(tag: string): unknown { if (tag === WS_METHODS.gitListBranches) { return { isRepo: true, + hasOriginRemote: true, branches: [ { name: "main", diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 108cbe798fc..dd089593649 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -348,6 +348,16 @@ describe("when: working tree has local changes", () => { }); }); + it("resolveQuickAction falls back to commit when no origin remote exists", () => { + const quick = resolveQuickAction(status({ hasWorkingTreeChanges: true, hasUpstream: false }), false, false, false); + assert.deepInclude(quick, { + kind: "run_action", + action: "commit", + label: "Commit", + disabled: false, + }); + }); + it("resolveQuickAction returns commit and push when open PR exists", () => { const quick = resolveQuickAction( status({ @@ -626,6 +636,25 @@ describe("when: branch has no upstream configured", () => { }); }); + it("resolveQuickAction disables push-and-pr flows when no origin remote exists", () => { + const quick = resolveQuickAction( + status({ + hasUpstream: false, + aheadCount: 2, + pr: null, + }), + false, + false, + false, + ); + assert.deepEqual(quick, { + kind: "show_hint", + label: "Push", + hint: 'Add an "origin" remote before pushing or creating a PR.', + disabled: true, + }); + }); + it("buildMenuItems enables create PR when no upstream and commits are ahead", () => { const items = buildMenuItems( status({ hasUpstream: false, pr: null, aheadCount: 2 }), @@ -659,6 +688,40 @@ describe("when: branch has no upstream configured", () => { ]); }); + it("buildMenuItems disables push and create PR when no origin remote exists", () => { + const items = buildMenuItems( + status({ hasUpstream: false, pr: null, aheadCount: 2 }), + false, + false, + ); + assert.deepEqual(items, [ + { + id: "commit", + label: "Commit", + disabled: true, + icon: "commit", + kind: "open_dialog", + dialogAction: "commit", + }, + { + id: "push", + label: "Push", + disabled: true, + icon: "push", + kind: "open_dialog", + dialogAction: "push", + }, + { + id: "pr", + label: "Create PR", + disabled: true, + icon: "pr", + kind: "open_dialog", + dialogAction: "create_pr", + }, + ]); + }); + it("resolveQuickAction is disabled on default branch when no upstream exists and no commits are ahead", () => { const quick = resolveQuickAction( status({ diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 4ad5e794067..6898a5ebdd3 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -113,6 +113,7 @@ export function summarizeGitResult(result: GitRunStackedActionResult): { export function buildMenuItems( gitStatus: GitStatusResult | null, isBusy: boolean, + hasOriginRemote = true, ): GitActionMenuItem[] { if (!gitStatus) return []; @@ -120,15 +121,23 @@ export function buildMenuItems( const hasChanges = gitStatus.hasWorkingTreeChanges; const hasOpenPr = gitStatus.pr?.state === "open"; const isBehind = gitStatus.behindCount > 0; + const canPushWithoutUpstream = hasOriginRemote && !gitStatus.hasUpstream; const canCommit = !isBusy && hasChanges; - const canPush = !isBusy && hasBranch && !hasChanges && !isBehind && gitStatus.aheadCount > 0; + const canPush = + !isBusy && + hasBranch && + !hasChanges && + !isBehind && + gitStatus.aheadCount > 0 && + (gitStatus.hasUpstream || canPushWithoutUpstream); const canCreatePr = !isBusy && hasBranch && !hasChanges && !hasOpenPr && gitStatus.aheadCount > 0 && - !isBehind; + !isBehind && + (gitStatus.hasUpstream || canPushWithoutUpstream); const canOpenPr = !isBusy && hasOpenPr; return [ @@ -171,6 +180,7 @@ export function resolveQuickAction( gitStatus: GitStatusResult | null, isBusy: boolean, isDefaultBranch = false, + hasOriginRemote = true, ): GitQuickAction { if (isBusy) { return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git action in progress." }; @@ -202,6 +212,9 @@ export function resolveQuickAction( } if (hasChanges) { + if (!gitStatus.hasUpstream && !hasOriginRemote) { + return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; + } if (hasOpenPr || isDefaultBranch) { return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" }; } @@ -214,6 +227,17 @@ export function resolveQuickAction( } if (!gitStatus.hasUpstream) { + if (!hasOriginRemote) { + if (hasOpenPr && !isAhead) { + return { label: "View PR", disabled: false, kind: "open_pr" }; + } + return { + label: "Push", + disabled: true, + kind: "show_hint", + hint: 'Add an "origin" remote before pushing or creating a PR.', + }; + } if (!isAhead) { if (hasOpenPr) { return { label: "View PR", disabled: false, kind: "open_pr" }; diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 4f183e3935f..0e3dfe7ac71 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -63,6 +63,7 @@ function getMenuActionDisabledReason( item: GitActionMenuItem, gitStatus: GitStatusResult | null, isBusy: boolean, + hasOriginRemote: boolean, ): string | null { if (!item.disabled) return null; if (isBusy) return "Git action in progress."; @@ -91,6 +92,9 @@ function getMenuActionDisabledReason( if (isBehind) { return "Branch is behind upstream. Pull/rebase before pushing."; } + if (!gitStatus.hasUpstream && !hasOriginRemote) { + return 'Add an "origin" remote before pushing.'; + } if (!isAhead) { return "No local commits to push."; } @@ -106,6 +110,9 @@ function getMenuActionDisabledReason( if (hasChanges) { return "Commit local changes before creating a PR."; } + if (!gitStatus.hasUpstream && !hasOriginRemote) { + return 'Add an "origin" remote before creating a PR.'; + } if (!isAhead) { return "No local commits to include in a PR."; } @@ -154,6 +161,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const { data: branchList = null } = useQuery(gitBranchesQueryOptions(gitCwd)); // Default to true while loading so we don't flash init controls. const isRepo = branchList?.isRepo ?? true; + const hasOriginRemote = branchList?.hasOriginRemote ?? false; const currentBranch = branchList?.branches.find((branch) => branch.current)?.name ?? null; const isGitStatusOutOfSync = !!gitStatus?.branch && !!currentBranch && gitStatus.branch !== currentBranch; @@ -184,12 +192,12 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions }, [branchList?.branches, gitStatusForActions?.branch]); const gitActionMenuItems = useMemo( - () => buildMenuItems(gitStatusForActions, isGitActionRunning), - [gitStatusForActions, isGitActionRunning], + () => buildMenuItems(gitStatusForActions, isGitActionRunning, hasOriginRemote), + [gitStatusForActions, hasOriginRemote, isGitActionRunning], ); const quickAction = useMemo( - () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch), - [gitStatusForActions, isDefaultBranch, isGitActionRunning], + () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch, hasOriginRemote), + [gitStatusForActions, hasOriginRemote, isDefaultBranch, isGitActionRunning], ); const quickActionDisabledReason = quickAction.disabled ? (quickAction.hint ?? "This action is currently unavailable.") @@ -645,6 +653,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions item, gitStatusForActions, isGitActionRunning, + hasOriginRemote, ); if (item.disabled && disabledReason) { return ( diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 80ede248e68..f5d3f372603 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -128,6 +128,7 @@ export type GitStatusResult = typeof GitStatusResult.Type; export const GitListBranchesResult = Schema.Struct({ branches: Schema.Array(GitBranch), isRepo: Schema.Boolean, + hasOriginRemote: Schema.Boolean, }); export type GitListBranchesResult = typeof GitListBranchesResult.Type; From d550044310f7f325644983e812bb99a3a313e726 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Mar 2026 09:39:31 -0700 Subject: [PATCH 2/3] fmt --- apps/desktop/scripts/dev-electron.mjs | 22 +- apps/desktop/src/main.ts | 24 +- apps/desktop/src/updateMachine.test.ts | 5 +- apps/desktop/src/updateMachine.ts | 1 - .../TestProviderAdapter.integration.ts | 603 ++++++----- apps/server/scripts/cli.ts | 9 +- apps/server/src/attachmentStore.ts | 3 +- apps/server/src/codexAppServerManager.test.ts | 126 ++- .../git/Layers/CodexTextGeneration.test.ts | 7 +- apps/server/src/git/Layers/GitCore.test.ts | 50 +- apps/server/src/git/Layers/GitManager.test.ts | 14 +- apps/server/src/keybindings.test.ts | 4 +- apps/server/src/open.ts | 8 +- .../Layers/CheckpointReactor.test.ts | 22 +- .../orchestration/Layers/CheckpointReactor.ts | 12 +- .../Layers/ProjectionPipeline.test.ts | 822 ++++++++------- .../Layers/ProjectionPipeline.ts | 6 +- .../Layers/ProjectionSnapshotQuery.test.ts | 9 +- .../Layers/ProviderCommandReactor.test.ts | 26 +- .../Layers/ProviderCommandReactor.ts | 48 +- .../Layers/ProviderRuntimeIngestion.test.ts | 11 +- .../Layers/ProviderRuntimeIngestion.ts | 51 +- apps/server/src/orchestration/decider.ts | 4 +- apps/server/src/orchestration/projector.ts | 7 +- .../Layers/ProjectionThreadProposedPlans.ts | 4 +- apps/server/src/projectFaviconRoute.test.ts | 14 +- .../src/provider/Layers/CodexAdapter.ts | 12 +- .../Layers/ProviderAdapterRegistry.test.ts | 5 +- .../Layers/ProviderAdapterRegistry.ts | 5 +- .../provider/Layers/ProviderHealth.test.ts | 43 +- .../provider/Layers/ProviderService.test.ts | 1 - .../src/provider/Layers/ProviderService.ts | 42 +- .../Layers/ProviderSessionDirectory.ts | 9 +- .../src/provider/Services/ProviderAdapter.ts | 9 +- apps/server/src/wsServer.test.ts | 32 +- apps/server/src/wsServer.ts | 26 +- apps/web/public/mockServiceWorker.js | 199 ++-- apps/web/src/appSettings.test.ts | 14 +- apps/web/src/appSettings.ts | 4 +- apps/web/src/components/ChatView.browser.tsx | 26 +- apps/web/src/components/ChatView.tsx | 937 +++++++++--------- .../src/components/ComposerPromptEditor.tsx | 96 +- apps/web/src/components/DiffPanel.tsx | 9 +- .../GitActionsControl.logic.test.ts | 17 +- apps/web/src/components/GitActionsControl.tsx | 31 +- apps/web/src/components/Icons.tsx | 65 +- apps/web/src/components/PlanSidebar.tsx | 17 +- .../components/desktopUpdate.logic.test.ts | 6 +- apps/web/src/composer-logic.ts | 11 +- apps/web/src/composerDraftStore.test.ts | 10 +- apps/web/src/composerDraftStore.ts | 19 +- apps/web/src/lib/turnDiffTree.test.ts | 7 +- apps/web/src/pendingUserInput.ts | 4 +- apps/web/src/proposedPlan.ts | 5 +- apps/web/src/routes/_chat.$threadId.tsx | 4 +- apps/web/src/routes/_chat.settings.tsx | 104 +- apps/web/src/session-logic.ts | 27 +- apps/web/src/terminalStateStore.test.ts | 37 +- apps/web/src/terminalStateStore.ts | 38 +- apps/web/src/wsNativeApi.test.ts | 21 +- packages/contracts/src/git.ts | 6 +- packages/contracts/src/orchestration.ts | 4 +- packages/contracts/src/project.ts | 4 +- packages/contracts/src/providerRuntime.ts | 35 +- packages/shared/src/shell.test.ts | 4 +- scripts/build-desktop-artifact.ts | 6 +- scripts/dev-runner.ts | 4 +- scripts/release-smoke.ts | 37 +- scripts/update-release-package-versions.ts | 3 +- 69 files changed, 1960 insertions(+), 1947 deletions(-) diff --git a/apps/desktop/scripts/dev-electron.mjs b/apps/desktop/scripts/dev-electron.mjs index 5d8bbe1116e..cf5dc26696d 100644 --- a/apps/desktop/scripts/dev-electron.mjs +++ b/apps/desktop/scripts/dev-electron.mjs @@ -7,7 +7,11 @@ import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs"; const port = Number(process.env.ELECTRON_RENDERER_PORT ?? 5733); const devServerUrl = `http://localhost:${port}`; -const requiredFiles = ["dist-electron/main.js", "dist-electron/preload.js", "../server/dist/index.mjs"]; +const requiredFiles = [ + "dist-electron/main.js", + "dist-electron/preload.js", + "../server/dist/index.mjs", +]; const watchedDirectories = [ { directory: "dist-electron", files: new Set(["main.js", "preload.js"]) }, { directory: "../server/dist", files: new Set(["index.mjs"]) }, @@ -148,13 +152,17 @@ function scheduleRestart() { function startWatchers() { for (const { directory, files } of watchedDirectories) { - const watcher = watch(join(desktopDir, directory), { persistent: true }, (_eventType, filename) => { - if (typeof filename !== "string" || !files.has(filename)) { - return; - } + const watcher = watch( + join(desktopDir, directory), + { persistent: true }, + (_eventType, filename) => { + if (typeof filename !== "string" || !files.has(filename)) { + return; + } - scheduleRestart(); - }); + scheduleRestart(); + }, + ); watchers.push(watcher); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 2abce92ba98..702dd900f57 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -15,10 +15,7 @@ import { NetService } from "@t3tools/shared/Net"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { fixPath } from "./fixPath"; -import { - getAutoUpdateDisabledReason, - shouldBroadcastDownloadProgress, -} from "./updateState"; +import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; import { createInitialDesktopUpdateState, reduceDesktopUpdateStateOnCheckFailure, @@ -80,7 +77,8 @@ let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; let destructiveMenuIconCache: Electron.NativeImage | null | undefined; -const initialUpdateState = (): DesktopUpdateState => createInitialDesktopUpdateState(app.getVersion()); +const initialUpdateState = (): DesktopUpdateState => + createInitialDesktopUpdateState(app.getVersion()); function logTimestamp(): string { return new Date().toISOString(); @@ -695,7 +693,9 @@ async function checkForUpdates(reason: string): Promise { await autoUpdater.checkForUpdates(); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); - setUpdateState(reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString())); + setUpdateState( + reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()), + ); console.error(`[desktop-updater] Failed to check for updates: ${message}`); } finally { updateCheckInFlight = false; @@ -756,9 +756,7 @@ function configureAutoUpdater(): void { updaterConfigured = true; const githubToken = - process.env.T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || - process.env.GH_TOKEN?.trim() || - ""; + process.env.T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || ""; if (githubToken) { // When a token is provided, re-configure the feed with `private: true` so // electron-updater uses the GitHub API (api.github.com) instead of the @@ -786,7 +784,13 @@ function configureAutoUpdater(): void { console.info("[desktop-updater] Looking for updates..."); }); autoUpdater.on("update-available", (info) => { - setUpdateState(reduceDesktopUpdateStateOnUpdateAvailable(updateState, info.version, new Date().toISOString())); + setUpdateState( + reduceDesktopUpdateStateOnUpdateAvailable( + updateState, + info.version, + new Date().toISOString(), + ), + ); lastLoggedDownloadMilestone = -1; console.info(`[desktop-updater] Update available: ${info.version}`); }); diff --git a/apps/desktop/src/updateMachine.test.ts b/apps/desktop/src/updateMachine.test.ts index 045bfaf8f4d..20e8af92a92 100644 --- a/apps/desktop/src/updateMachine.test.ts +++ b/apps/desktop/src/updateMachine.test.ts @@ -77,7 +77,10 @@ describe("updateMachine", () => { }, "1.1.0", ); - const failedInstall = reduceDesktopUpdateStateOnInstallFailure(downloaded, "backend shutdown timed out"); + const failedInstall = reduceDesktopUpdateStateOnInstallFailure( + downloaded, + "backend shutdown timed out", + ); expect(downloaded.status).toBe("downloaded"); expect(downloaded.downloadedVersion).toBe("1.1.0"); diff --git a/apps/desktop/src/updateMachine.ts b/apps/desktop/src/updateMachine.ts index 9c4e2126577..c82d0dfe3f7 100644 --- a/apps/desktop/src/updateMachine.ts +++ b/apps/desktop/src/updateMachine.ts @@ -152,4 +152,3 @@ export function reduceDesktopUpdateStateOnInstallFailure( canRetry: true, }; } - diff --git a/apps/server/integration/TestProviderAdapter.integration.ts b/apps/server/integration/TestProviderAdapter.integration.ts index 82c08da3e95..017c59e2c8b 100644 --- a/apps/server/integration/TestProviderAdapter.integration.ts +++ b/apps/server/integration/TestProviderAdapter.integration.ts @@ -72,10 +72,9 @@ function normalizeTurnState(value: unknown): "completed" | "failed" | "interrupt return "completed"; } -function mapRequestType(requestKind: unknown): - | "command_execution_approval" - | "file_change_approval" - | "unknown" { +function mapRequestType( + requestKind: unknown, +): "command_execution_approval" | "file_change_approval" | "unknown" { if (requestKind === "command") { return "command_execution_approval"; } @@ -239,323 +238,323 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter }> >(); - const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); + const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); - const startSession: ProviderAdapterShape["startSession"] = (input) => - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== provider) { - return yield* new ProviderAdapterValidationError({ - provider, - operation: "startSession", - issue: `Expected provider '${provider}' but received '${input.provider}'.`, - }); - } + const startSession: ProviderAdapterShape["startSession"] = (input) => + Effect.gen(function* () { + if (input.provider !== undefined && input.provider !== provider) { + return yield* new ProviderAdapterValidationError({ + provider, + operation: "startSession", + issue: `Expected provider '${provider}' but received '${input.provider}'.`, + }); + } - sessionCount += 1; - const threadId = input.threadId; - const createdAt = nowIso(); - - const session: ProviderSession = { - provider, - status: "ready", - runtimeMode: input.runtimeMode, - threadId, - cwd: input.cwd, - resumeCursor: input.resumeCursor ?? { threadId: String(threadId), seed: sessionCount }, - createdAt, - updatedAt: createdAt, - }; - - sessions.set(threadId, { - session, - snapshot: { + sessionCount += 1; + const threadId = input.threadId; + const createdAt = nowIso(); + + const session: ProviderSession = { + provider, + status: "ready", + runtimeMode: input.runtimeMode, threadId, - turns: [], - }, - turnCount: 0, - queuedResponses: queuedResponsesForNextSession.splice(0), - rollbackCalls: [], - }); + cwd: input.cwd, + resumeCursor: input.resumeCursor ?? { threadId: String(threadId), seed: sessionCount }, + createdAt, + updatedAt: createdAt, + }; - return session; - }); + sessions.set(threadId, { + session, + snapshot: { + threadId, + turns: [], + }, + turnCount: 0, + queuedResponses: queuedResponsesForNextSession.splice(0), + rollbackCalls: [], + }); - const sendTurn: ProviderAdapterShape["sendTurn"] = (input) => - Effect.gen(function* () { - const state = sessions.get(input.threadId); - if (!state) { - return yield* missingSessionEffect(provider, input.threadId); - } + return session; + }); - state.turnCount += 1; - const turnCount = state.turnCount; - const turnId = TurnId.makeUnsafe(`turn-${turnCount}`); + const sendTurn: ProviderAdapterShape["sendTurn"] = (input) => + Effect.gen(function* () { + const state = sessions.get(input.threadId); + if (!state) { + return yield* missingSessionEffect(provider, input.threadId); + } - const response = state.queuedResponses.shift(); - if (!response) { - return yield* new ProviderAdapterValidationError({ - provider, - operation: "sendTurn", - issue: `No queued turn response for thread ${input.threadId}.`, - }); - } + state.turnCount += 1; + const turnCount = state.turnCount; + const turnId = TurnId.makeUnsafe(`turn-${turnCount}`); - const assistantDeltas: string[] = []; - const deferredTurnCompletedEvents: ProviderRuntimeEvent[] = []; - for (const fixtureEvent of response.events) { - const rawEvent: Record = { - ...(fixtureEvent as Record), - eventId: randomUUID(), - provider, - sessionId: RuntimeSessionId.makeUnsafe(String(input.threadId)), - createdAt: nowIso(), - }; - rawEvent.threadId = state.snapshot.threadId; - if (Object.hasOwn(rawEvent, "turnId")) { - rawEvent.turnId = turnId; + const response = state.queuedResponses.shift(); + if (!response) { + return yield* new ProviderAdapterValidationError({ + provider, + operation: "sendTurn", + issue: `No queued turn response for thread ${input.threadId}.`, + }); } - const runtimeEvent = normalizeFixtureEvent(rawEvent); - const runtimeType = (runtimeEvent as { type: string }).type; - if (runtimeType === "content.delta") { - const payload = runtimeEvent.payload as { delta?: unknown } | undefined; - if (typeof payload?.delta === "string") { - assistantDeltas.push(payload.delta); + const assistantDeltas: string[] = []; + const deferredTurnCompletedEvents: ProviderRuntimeEvent[] = []; + for (const fixtureEvent of response.events) { + const rawEvent: Record = { + ...(fixtureEvent as Record), + eventId: randomUUID(), + provider, + sessionId: RuntimeSessionId.makeUnsafe(String(input.threadId)), + createdAt: nowIso(), + }; + rawEvent.threadId = state.snapshot.threadId; + if (Object.hasOwn(rawEvent, "turnId")) { + rawEvent.turnId = turnId; + } + + const runtimeEvent = normalizeFixtureEvent(rawEvent); + const runtimeType = (runtimeEvent as { type: string }).type; + if (runtimeType === "content.delta") { + const payload = runtimeEvent.payload as { delta?: unknown } | undefined; + if (typeof payload?.delta === "string") { + assistantDeltas.push(payload.delta); + } + } else if (runtimeType === "message.delta") { + const legacyDelta = (runtimeEvent as { delta?: unknown }).delta; + if (typeof legacyDelta === "string") { + assistantDeltas.push(legacyDelta); + } } - } else if (runtimeType === "message.delta") { - const legacyDelta = (runtimeEvent as { delta?: unknown }).delta; - if (typeof legacyDelta === "string") { - assistantDeltas.push(legacyDelta); + if (runtimeEvent.type === "turn.completed") { + deferredTurnCompletedEvents.push(runtimeEvent); + continue; } + + yield* emit(runtimeEvent); } - if (runtimeEvent.type === "turn.completed") { - deferredTurnCompletedEvents.push(runtimeEvent); - continue; + + if (response.mutateWorkspace && state.session.cwd) { + yield* response.mutateWorkspace({ cwd: state.session.cwd!, turnCount }); } - yield* emit(runtimeEvent); - } + const userItem = { + type: "userMessage", + content: [{ type: "text", text: input.input }], + } as const; + const assistantText = assistantDeltas.join(""); + const nextItems: Array = + assistantText.length > 0 + ? [userItem, { type: "agentMessage", text: assistantText }] + : [userItem]; + + const nextTurn: ProviderThreadTurnSnapshot = { + id: turnId, + items: nextItems, + }; - if (response.mutateWorkspace && state.session.cwd) { - yield* response.mutateWorkspace({ cwd: state.session.cwd!, turnCount }); - } + state.snapshot = { + threadId: state.snapshot.threadId, + turns: [...state.snapshot.turns, nextTurn], + }; - const userItem = { - type: "userMessage", - content: [{ type: "text", text: input.input }], - } as const; - const assistantText = assistantDeltas.join(""); - const nextItems: Array = - assistantText.length > 0 - ? [userItem, { type: "agentMessage", text: assistantText }] - : [userItem]; - - const nextTurn: ProviderThreadTurnSnapshot = { - id: turnId, - items: nextItems, - }; - - state.snapshot = { - threadId: state.snapshot.threadId, - turns: [...state.snapshot.turns, nextTurn], - }; - - if (deferredTurnCompletedEvents.length === 0) { - yield* emit({ - type: "turn.completed", - eventId: EventId.makeUnsafe(randomUUID()), - provider, - createdAt: nowIso(), + if (deferredTurnCompletedEvents.length === 0) { + yield* emit({ + type: "turn.completed", + eventId: EventId.makeUnsafe(randomUUID()), + provider, + createdAt: nowIso(), + threadId: state.snapshot.threadId, + turnId, + payload: { + state: "completed", + }, + }); + } else { + for (const completedEvent of deferredTurnCompletedEvents) { + yield* emit(completedEvent); + } + } + + return { threadId: state.snapshot.threadId, turnId, - payload: { - state: "completed", - }, - }); - } else { - for (const completedEvent of deferredTurnCompletedEvents) { - yield* emit(completedEvent); - } + } satisfies ProviderTurnStartResult; + }); + + const interruptTurn: ProviderAdapterShape["interruptTurn"] = ( + threadId, + turnId, + ) => + sessions.has(threadId) + ? Effect.sync(() => { + const existing = interruptCallsBySession.get(threadId) ?? []; + existing.push(turnId); + interruptCallsBySession.set(threadId, existing); + }) + : missingSessionEffect(provider, threadId); + + const respondToRequest: ProviderAdapterShape["respondToRequest"] = ( + threadId, + requestId, + decision, + ) => + sessions.has(threadId) + ? Effect.sync(() => { + const existing = approvalResponsesBySession.get(threadId) ?? []; + existing.push({ + threadId, + requestId, + decision, + }); + approvalResponsesBySession.set(threadId, existing); + }) + : missingSessionEffect(provider, threadId); + + const respondToUserInput: ProviderAdapterShape["respondToUserInput"] = ( + threadId, + _requestId, + _answers, + ) => (sessions.has(threadId) ? Effect.void : missingSessionEffect(provider, threadId)); + + const stopSession: ProviderAdapterShape["stopSession"] = (threadId) => + Effect.sync(() => { + sessions.delete(threadId); + }); + + const listSessions: ProviderAdapterShape["listSessions"] = () => + Effect.sync(() => Array.from(sessions.values(), (state) => state.session)); + + const hasSession: ProviderAdapterShape["hasSession"] = (threadId) => + Effect.succeed(sessions.has(threadId)); + + const readThread: ProviderAdapterShape["readThread"] = (threadId) => { + const state = sessions.get(threadId); + if (!state) { + return missingSessionEffect(provider, threadId); + } + return Effect.succeed(state.snapshot); + }; + + const rollbackThread: ProviderAdapterShape["rollbackThread"] = ( + threadId, + numTurns, + ) => { + const state = sessions.get(threadId); + if (!state) { + return missingSessionEffect(provider, threadId); + } + if (!Number.isInteger(numTurns) || numTurns < 0 || numTurns > state.snapshot.turns.length) { + return Effect.fail( + new ProviderAdapterValidationError({ + provider, + operation: "rollbackThread", + issue: "numTurns must be an integer between 0 and current turn count.", + }), + ); } - return { - threadId: state.snapshot.threadId, - turnId, - } satisfies ProviderTurnStartResult; - }); - - const interruptTurn: ProviderAdapterShape["interruptTurn"] = ( - threadId, - turnId, - ) => - sessions.has(threadId) - ? Effect.sync(() => { - const existing = interruptCallsBySession.get(threadId) ?? []; - existing.push(turnId); - interruptCallsBySession.set(threadId, existing); - }) - : missingSessionEffect(provider, threadId); - - const respondToRequest: ProviderAdapterShape["respondToRequest"] = ( - threadId, - requestId, - decision, - ) => - sessions.has(threadId) - ? Effect.sync(() => { - const existing = approvalResponsesBySession.get(threadId) ?? []; - existing.push({ - threadId, - requestId, - decision, - }); - approvalResponsesBySession.set(threadId, existing); - }) - : missingSessionEffect(provider, threadId); - - const respondToUserInput: ProviderAdapterShape["respondToUserInput"] = ( - threadId, - _requestId, - _answers, - ) => (sessions.has(threadId) ? Effect.void : missingSessionEffect(provider, threadId)); - - const stopSession: ProviderAdapterShape["stopSession"] = (threadId) => - Effect.sync(() => { - sessions.delete(threadId); - }); - - const listSessions: ProviderAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), (state) => state.session)); - - const hasSession: ProviderAdapterShape["hasSession"] = (threadId) => - Effect.succeed(sessions.has(threadId)); - - const readThread: ProviderAdapterShape["readThread"] = (threadId) => { - const state = sessions.get(threadId); - if (!state) { - return missingSessionEffect(provider, threadId); - } - return Effect.succeed(state.snapshot); - }; - - const rollbackThread: ProviderAdapterShape["rollbackThread"] = ( - threadId, - numTurns, - ) => { - const state = sessions.get(threadId); - if (!state) { - return missingSessionEffect(provider, threadId); - } - if (!Number.isInteger(numTurns) || numTurns < 0 || numTurns > state.snapshot.turns.length) { - return Effect.fail( - new ProviderAdapterValidationError({ - provider, - operation: "rollbackThread", - issue: "numTurns must be an integer between 0 and current turn count.", - }), + return Effect.sync(() => { + state.rollbackCalls.push(numTurns); + state.snapshot = { + threadId: state.snapshot.threadId, + turns: state.snapshot.turns.slice(0, state.snapshot.turns.length - numTurns), + }; + state.turnCount = state.snapshot.turns.length; + return state.snapshot; + }); + }; + + const stopAll: ProviderAdapterShape["stopAll"] = () => + Effect.sync(() => { + sessions.clear(); + }); + + const adapter: ProviderAdapterShape = { + provider, + capabilities: { + sessionModelSwitch: "in-session", + }, + startSession, + sendTurn, + interruptTurn, + respondToRequest, + respondToUserInput, + stopSession, + listSessions, + hasSession, + readThread, + rollbackThread, + stopAll, + streamEvents: Stream.fromQueue(runtimeEvents), + }; + + const queueTurnResponse = ( + threadId: ThreadId, + response: TestTurnResponse, + ): Effect.Effect => + Effect.sync(() => sessions.get(threadId)).pipe( + Effect.flatMap((state) => + state + ? Effect.sync(() => { + state.queuedResponses.push(response); + }) + : Effect.fail(sessionNotFound(provider, threadId)), + ), ); - } - - return Effect.sync(() => { - state.rollbackCalls.push(numTurns); - state.snapshot = { - threadId: state.snapshot.threadId, - turns: state.snapshot.turns.slice(0, state.snapshot.turns.length - numTurns), - }; - state.turnCount = state.snapshot.turns.length; - return state.snapshot; - }); - }; - - const stopAll: ProviderAdapterShape["stopAll"] = () => - Effect.sync(() => { - sessions.clear(); - }); - - const adapter: ProviderAdapterShape = { - provider, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - readThread, - rollbackThread, - stopAll, - streamEvents: Stream.fromQueue(runtimeEvents), - }; - - const queueTurnResponse = ( - threadId: ThreadId, - response: TestTurnResponse, - ): Effect.Effect => - Effect.sync(() => sessions.get(threadId)).pipe( - Effect.flatMap((state) => - state - ? Effect.sync(() => { - state.queuedResponses.push(response); - }) - : Effect.fail(sessionNotFound(provider, threadId)), - ), - ); - - const queueTurnResponseForNextSession = ( - response: TestTurnResponse, - ): Effect.Effect => - Effect.sync(() => { - queuedResponsesForNextSession.push(response); - }); - - const getRollbackCalls = (threadId: ThreadId): ReadonlyArray => { - const state = sessions.get(threadId); - if (!state) { - return []; - } - return [...state.rollbackCalls]; - }; - - const getStartCount = (): number => sessionCount; - - const getInterruptCalls = (threadId: ThreadId): ReadonlyArray => { - const calls = interruptCallsBySession.get(threadId); - if (!calls) { - return []; - } - return [...calls]; - }; - - const listActiveSessionIds = (): ReadonlyArray => - Array.from(sessions.values(), (state) => state.session.threadId); - - const getApprovalResponses = ( - threadId: ThreadId, - ): ReadonlyArray<{ - readonly threadId: ThreadId; - readonly requestId: ApprovalRequestId; - readonly decision: ProviderApprovalDecision; - }> => { - const responses = approvalResponsesBySession.get(threadId); - if (!responses) { - return []; - } - return [...responses]; - }; - - return { - adapter, - provider, - queueTurnResponse, - queueTurnResponseForNextSession, - getStartCount, - getRollbackCalls, - getInterruptCalls, - listActiveSessionIds, - getApprovalResponses, - } satisfies TestProviderAdapterHarness; -}); + + const queueTurnResponseForNextSession = ( + response: TestTurnResponse, + ): Effect.Effect => + Effect.sync(() => { + queuedResponsesForNextSession.push(response); + }); + + const getRollbackCalls = (threadId: ThreadId): ReadonlyArray => { + const state = sessions.get(threadId); + if (!state) { + return []; + } + return [...state.rollbackCalls]; + }; + + const getStartCount = (): number => sessionCount; + + const getInterruptCalls = (threadId: ThreadId): ReadonlyArray => { + const calls = interruptCallsBySession.get(threadId); + if (!calls) { + return []; + } + return [...calls]; + }; + + const listActiveSessionIds = (): ReadonlyArray => + Array.from(sessions.values(), (state) => state.session.threadId); + + const getApprovalResponses = ( + threadId: ThreadId, + ): ReadonlyArray<{ + readonly threadId: ThreadId; + readonly requestId: ApprovalRequestId; + readonly decision: ProviderApprovalDecision; + }> => { + const responses = approvalResponsesBySession.get(threadId); + if (!responses) { + return []; + } + return [...responses]; + }; + + return { + adapter, + provider, + queueTurnResponse, + queueTurnResponseForNextSession, + getStartCount, + getRollbackCalls, + getInterruptCalls, + listActiveSessionIds, + getApprovalResponses, + } satisfies TestProviderAdapterHarness; + }); diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index 3eaa72ea8e7..21bc515aa7b 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -6,7 +6,10 @@ import { Data, Effect, FileSystem, Logger, Option, Path } from "effect"; import { Command, Flag } from "effect/unstable/cli"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { DEVELOPMENT_ICON_OVERRIDES, PUBLISH_ICON_OVERRIDES } from "../../../scripts/lib/brand-assets.ts"; +import { + DEVELOPMENT_ICON_OVERRIDES, + PUBLISH_ICON_OVERRIDES, +} from "../../../scripts/lib/brand-assets.ts"; import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; import rootPackageJson from "../../../package.json" with { type: "json" }; import serverPackageJson from "../package.json" with { type: "json" }; @@ -237,9 +240,7 @@ const publishCmd = Command.make( Effect.gen(function* () { yield* restorePublishIconOverrides(resource.iconBackups).pipe( Effect.catch((error) => - Effect.logError( - `[cli] Failed to restore publish icon overrides: ${String(error)}`, - ), + Effect.logError(`[cli] Failed to restore publish icon overrides: ${String(error)}`), ), ); yield* fs.rename(backupPath, packageJsonPath); diff --git a/apps/server/src/attachmentStore.ts b/apps/server/src/attachmentStore.ts index 48be2df8a6c..3440e29fc3e 100644 --- a/apps/server/src/attachmentStore.ts +++ b/apps/server/src/attachmentStore.ts @@ -12,8 +12,7 @@ import { inferImageExtension, SAFE_IMAGE_FILE_EXTENSIONS } from "./imageMime.ts" const ATTACHMENT_FILENAME_EXTENSIONS = [...SAFE_IMAGE_FILE_EXTENSIONS, ".bin"]; const ATTACHMENT_ID_THREAD_SEGMENT_MAX_CHARS = 80; const ATTACHMENT_ID_THREAD_SEGMENT_PATTERN = "[a-z0-9_]+(?:-[a-z0-9_]+)*"; -const ATTACHMENT_ID_UUID_PATTERN = - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; +const ATTACHMENT_ID_UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; const ATTACHMENT_ID_PATTERN = new RegExp( `^(${ATTACHMENT_ID_THREAD_SEGMENT_PATTERN})-(${ATTACHMENT_ID_UUID_PATTERN})$`, "i", diff --git a/apps/server/src/codexAppServerManager.test.ts b/apps/server/src/codexAppServerManager.test.ts index 4e0073d75ed..cea8df0a0b0 100644 --- a/apps/server/src/codexAppServerManager.test.ts +++ b/apps/server/src/codexAppServerManager.test.ts @@ -749,88 +749,82 @@ describe("respondToUserInput", () => { }); describe.skipIf(!process.env.CODEX_BINARY_PATH)("startSession live Codex resume", () => { - it( - "keeps prior thread history when resuming with a changed runtime mode", - async () => { - const workspaceDir = mkdtempSync(path.join(os.tmpdir(), "codex-live-resume-")); - writeFileSync(path.join(workspaceDir, "README.md"), "hello\n", "utf8"); + it("keeps prior thread history when resuming with a changed runtime mode", async () => { + const workspaceDir = mkdtempSync(path.join(os.tmpdir(), "codex-live-resume-")); + writeFileSync(path.join(workspaceDir, "README.md"), "hello\n", "utf8"); - const manager = new CodexAppServerManager(); + const manager = new CodexAppServerManager(); - try { - const firstSession = await manager.startSession({ - threadId: asThreadId("thread-live"), - provider: "codex", - cwd: workspaceDir, - runtimeMode: "full-access", - providerOptions: { - codex: { - ...(process.env.CODEX_BINARY_PATH - ? { binaryPath: process.env.CODEX_BINARY_PATH } - : {}), - ...(process.env.CODEX_HOME_PATH - ? { homePath: process.env.CODEX_HOME_PATH } - : {}), - }, + try { + const firstSession = await manager.startSession({ + threadId: asThreadId("thread-live"), + provider: "codex", + cwd: workspaceDir, + runtimeMode: "full-access", + providerOptions: { + codex: { + ...(process.env.CODEX_BINARY_PATH ? { binaryPath: process.env.CODEX_BINARY_PATH } : {}), + ...(process.env.CODEX_HOME_PATH ? { homePath: process.env.CODEX_HOME_PATH } : {}), }, - }); + }, + }); - const firstTurn = await manager.sendTurn({ - threadId: firstSession.threadId, - input: `Reply with exactly the word ALPHA ${randomUUID()}`, - }); + const firstTurn = await manager.sendTurn({ + threadId: firstSession.threadId, + input: `Reply with exactly the word ALPHA ${randomUUID()}`, + }); - expect(firstTurn.threadId).toBe(firstSession.threadId); + expect(firstTurn.threadId).toBe(firstSession.threadId); - await vi.waitFor(async () => { + await vi.waitFor( + async () => { const snapshot = await manager.readThread(firstSession.threadId); expect(snapshot.turns.length).toBeGreaterThan(0); - }, { timeout: 120_000, interval: 1_000 }); + }, + { timeout: 120_000, interval: 1_000 }, + ); - const firstSnapshot = await manager.readThread(firstSession.threadId); - const originalThreadId = firstSnapshot.threadId; - const originalTurnCount = firstSnapshot.turns.length; + const firstSnapshot = await manager.readThread(firstSession.threadId); + const originalThreadId = firstSnapshot.threadId; + const originalTurnCount = firstSnapshot.turns.length; - manager.stopSession(firstSession.threadId); + manager.stopSession(firstSession.threadId); - const resumedSession = await manager.startSession({ - threadId: firstSession.threadId, - provider: "codex", - cwd: workspaceDir, - runtimeMode: "approval-required", - resumeCursor: firstSession.resumeCursor, - providerOptions: { - codex: { - ...(process.env.CODEX_BINARY_PATH - ? { binaryPath: process.env.CODEX_BINARY_PATH } - : {}), - ...(process.env.CODEX_HOME_PATH - ? { homePath: process.env.CODEX_HOME_PATH } - : {}), - }, + const resumedSession = await manager.startSession({ + threadId: firstSession.threadId, + provider: "codex", + cwd: workspaceDir, + runtimeMode: "approval-required", + resumeCursor: firstSession.resumeCursor, + providerOptions: { + codex: { + ...(process.env.CODEX_BINARY_PATH ? { binaryPath: process.env.CODEX_BINARY_PATH } : {}), + ...(process.env.CODEX_HOME_PATH ? { homePath: process.env.CODEX_HOME_PATH } : {}), }, - }); + }, + }); - expect(resumedSession.threadId).toBe(originalThreadId); + expect(resumedSession.threadId).toBe(originalThreadId); - const resumedSnapshotBeforeTurn = await manager.readThread(resumedSession.threadId); - expect(resumedSnapshotBeforeTurn.threadId).toBe(originalThreadId); - expect(resumedSnapshotBeforeTurn.turns.length).toBeGreaterThanOrEqual(originalTurnCount); + const resumedSnapshotBeforeTurn = await manager.readThread(resumedSession.threadId); + expect(resumedSnapshotBeforeTurn.threadId).toBe(originalThreadId); + expect(resumedSnapshotBeforeTurn.turns.length).toBeGreaterThanOrEqual(originalTurnCount); - await manager.sendTurn({ - threadId: resumedSession.threadId, - input: `Reply with exactly the word BETA ${randomUUID()}`, - }); + await manager.sendTurn({ + threadId: resumedSession.threadId, + input: `Reply with exactly the word BETA ${randomUUID()}`, + }); - await vi.waitFor(async () => { + await vi.waitFor( + async () => { const snapshot = await manager.readThread(resumedSession.threadId); expect(snapshot.turns.length).toBeGreaterThan(originalTurnCount); - }, { timeout: 120_000, interval: 1_000 }); - } finally { - manager.stopAll(); - rmSync(workspaceDir, { recursive: true, force: true }); - } - }, - 180_000, - ); + }, + { timeout: 120_000, interval: 1_000 }, + ); + } finally { + manager.stopAll(); + rmSync(workspaceDir, { recursive: true, force: true }); + } + }, 180_000); }); diff --git a/apps/server/src/git/Layers/CodexTextGeneration.test.ts b/apps/server/src/git/Layers/CodexTextGeneration.test.ts index 9642f0b06a4..1cf2d0e0922 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.test.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.test.ts @@ -363,8 +363,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const attachmentId = - `thread-1-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + const attachmentId = `thread-1-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const imagePath = path.join(process.cwd(), "attachments", `${attachmentId}.png`); yield* fs.makeDirectory(path.join(process.cwd(), "attachments"), { recursive: true }); yield* fs.writeFile(imagePath, Buffer.from("hello")); @@ -392,9 +391,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGenerationLive", (it) => { }), ), ), - Effect.ensuring( - fs.remove(imagePath).pipe(Effect.catch(() => Effect.void)), - ), + Effect.ensuring(fs.remove(imagePath).pipe(Effect.catch(() => Effect.void))), ); expect(generated.branch).toBe("fix/ui-regression"); diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 88327327332..d1c5a1c5f34 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -429,7 +429,9 @@ it.layer(TestLayer)("git integration", (it) => { true, ); expect( - result.branches.some((branch) => branch.name === "feature/local-only" && !branch.isRemote), + result.branches.some( + (branch) => branch.name === "feature/local-only" && !branch.isRemote, + ), ).toBe(true); expect( result.branches.some( @@ -692,29 +694,27 @@ it.layer(TestLayer)("git integration", (it) => { }), ); - it.effect( - "does not silently checkout a local branch when a remote ref no longer exists", - () => - Effect.gen(function* () { - const remote = yield* makeTmpDir(); - const source = yield* makeTmpDir(); - yield* git(remote, ["init", "--bare"]); + it.effect("does not silently checkout a local branch when a remote ref no longer exists", () => + Effect.gen(function* () { + const remote = yield* makeTmpDir(); + const source = yield* makeTmpDir(); + yield* git(remote, ["init", "--bare"]); - yield* initRepoWithCommit(source); - const defaultBranch = (yield* listGitBranches({ cwd: source })).branches.find( - (branch) => branch.current, - )!.name; - yield* git(source, ["remote", "add", "origin", remote]); - yield* git(source, ["push", "-u", "origin", defaultBranch]); + yield* initRepoWithCommit(source); + const defaultBranch = (yield* listGitBranches({ cwd: source })).branches.find( + (branch) => branch.current, + )!.name; + yield* git(source, ["remote", "add", "origin", remote]); + yield* git(source, ["push", "-u", "origin", defaultBranch]); - yield* createGitBranch({ cwd: source, branch: "feature" }); + yield* createGitBranch({ cwd: source, branch: "feature" }); - const checkoutResult = yield* Effect.result( - checkoutGitBranch({ cwd: source, branch: "origin/feature" }), - ); - expect(checkoutResult._tag).toBe("Failure"); - expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch); - }), + const checkoutResult = yield* Effect.result( + checkoutGitBranch({ cwd: source, branch: "origin/feature" }), + ); + expect(checkoutResult._tag).toBe("Failure"); + expect(yield* git(source, ["branch", "--show-current"])).toBe(defaultBranch); + }), ); it.effect("checks out a remote tracking branch when remote name contains slashes", () => @@ -943,13 +943,7 @@ it.layer(TestLayer)("git integration", (it) => { }); expect(renamed.branch).toBe("feature/new-name"); - expect(renameArgs).toEqual([ - "branch", - "-m", - "--", - "feature/old-name", - "feature/new-name", - ]); + expect(renameArgs).toEqual(["branch", "-m", "--", "feature/old-name", "feature/new-name"]); }), ); }); diff --git a/apps/server/src/git/Layers/GitManager.test.ts b/apps/server/src/git/Layers/GitManager.test.ts index d8a3753bb06..92d7b6c5984 100644 --- a/apps/server/src/git/Layers/GitManager.test.ts +++ b/apps/server/src/git/Layers/GitManager.test.ts @@ -82,7 +82,11 @@ function runGit( function initRepo( cwd: string, -): Effect.Effect { +): Effect.Effect< + void, + PlatformError.PlatformError | GitCommandError, + FileSystem.FileSystem | Scope.Scope | GitService +> { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; yield* runGit(cwd, ["init", "--initial-branch=main"]); @@ -112,9 +116,7 @@ function createTextGeneration(overrides: Partial = {}): T Effect.succeed({ subject: "Implement stacked git actions", body: "", - ...(input.includeBranch - ? { branch: "feature/implement-stacked-git-actions" } - : {}), + ...(input.includeBranch ? { branch: "feature/implement-stacked-git-actions" } : {}), }), generatePrContent: () => Effect.succeed({ @@ -556,9 +558,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => { return { subject: "Implement stacked git actions", body: "", - ...(input.includeBranch - ? { branch: "feature/implement-stacked-git-actions" } - : {}), + ...(input.includeBranch ? { branch: "feature/implement-stacked-git-actions" } : {}), }; }), }, diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 4a10d5816b4..6954cafc598 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -211,7 +211,9 @@ it.layer(NodeServices.layer)("keybindings", (it) => { }); assert.isTrue(configState.keybindings.some((entry) => entry.command === "terminal.toggle")); - assert.isFalse(configState.keybindings.some((entry) => String(entry.command) === "invalid.command")); + assert.isFalse( + configState.keybindings.some((entry) => String(entry.command) === "invalid.command"), + ); assert.deepEqual(configState.issues, [ { kind: "keybindings.invalid-entry", diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index 5c742fba9d6..e7238c04b29 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -40,7 +40,9 @@ interface CommandAvailabilityOptions { const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; function shouldUseGotoFlag(editorId: EditorId, target: string): boolean { - return (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target); + return ( + (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target) + ); } function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { @@ -239,9 +241,7 @@ export const launchDetached = (launch: EditorLaunch) => }); } catch (error) { return resume( - Effect.fail( - new OpenError({ message: "failed to spawn detached process", cause: error }), - ), + Effect.fail(new OpenError({ message: "failed to spawn detached process", cause: error })), ); } diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts index d675c85ff53..eecfc069d37 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.test.ts @@ -69,7 +69,7 @@ function createProviderServiceHarness( Effect.die(new Error("Unsupported provider call in test")) as Effect.Effect; const listSessions = () => hasSession - ? Effect.succeed([ + ? Effect.succeed([ { provider: providerName, status: "ready", @@ -350,7 +350,7 @@ describe("CheckpointReactor", () => { type: "turn.started", eventId: EventId.makeUnsafe("evt-turn-started-1"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-1"), @@ -365,7 +365,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-turn-completed-1"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-1"), @@ -426,7 +426,7 @@ describe("CheckpointReactor", () => { type: "turn.started", eventId: EventId.makeUnsafe("evt-turn-started-main"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-main"), @@ -442,7 +442,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-turn-completed-aux"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-aux"), @@ -460,7 +460,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-turn-completed-main"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-main"), @@ -500,7 +500,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-turn-completed-missing-baseline"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-missing-baseline"), @@ -589,7 +589,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-turn-completed-missing-provider-cwd"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-missing-cwd"), @@ -635,7 +635,7 @@ describe("CheckpointReactor", () => { type: "checkpoint.captured", eventId: EventId.makeUnsafe("evt-checkpoint-captured-3"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-3"), @@ -685,7 +685,7 @@ describe("CheckpointReactor", () => { type: "turn.completed", eventId: EventId.makeUnsafe("evt-runtime-capture-failure"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-runtime-failure"), @@ -696,7 +696,7 @@ describe("CheckpointReactor", () => { type: "turn.started", eventId: EventId.makeUnsafe("evt-turn-started-after-runtime-failure"), provider: "codex", - + createdAt: new Date().toISOString(), threadId: ThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-after-runtime-failure"), diff --git a/apps/server/src/orchestration/Layers/CheckpointReactor.ts b/apps/server/src/orchestration/Layers/CheckpointReactor.ts index 52243248fb0..da0e08b9310 100644 --- a/apps/server/src/orchestration/Layers/CheckpointReactor.ts +++ b/apps/server/src/orchestration/Layers/CheckpointReactor.ts @@ -115,9 +115,7 @@ const make = Effect.gen(function* () { const resolveSessionRuntimeForThread = Effect.fnUntraced(function* ( threadId: ThreadId, - ): Effect.fn.Return< - Option.Option<{ readonly threadId: ThreadId; readonly cwd: string }> - > { + ): Effect.fn.Return> { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); @@ -133,9 +131,7 @@ const make = Effect.gen(function* () { }; if (thread) { - const projectedSession = sessions.find( - (session) => session.threadId === thread.id, - ); + const projectedSession = sessions.find((session) => session.threadId === thread.id); const fromProjected = findSessionWithCwd(projectedSession); if (Option.isSome(fromProjected)) { return fromProjected; @@ -306,9 +302,7 @@ const make = Effect.gen(function* () { } const readModel = yield* orchestrationEngine.getReadModel(); - const thread = readModel.threads.find( - (entry) => entry.id === event.threadId, - ); + const thread = readModel.threads.find((entry) => entry.id === event.threadId); if (!thread) { return; } diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index 6e4e824f396..d15b2efa2e9 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -54,9 +54,7 @@ const runWithProjectionPipelineLayer = ( (runtime) => Effect.promise(() => runtime.dispose()), ); -const projectionLayer = it.layer( - makeProjectionPipelineTestLayer(process.cwd()), -); +const projectionLayer = it.layer(makeProjectionPipelineTestLayer(process.cwd())); projectionLayer("OrchestrationProjectionPipeline", (it) => { it.effect("bootstraps all projection states and writes projection rows", () => @@ -450,153 +448,149 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { }), ); - it.effect( - "overwrites stored attachment references when a message updates attachments", - () => - Effect.sync(() => - fs.mkdtempSync(path.join(os.tmpdir(), "t3-projection-attachments-overwrite-")), - ).pipe( - Effect.flatMap((stateDir) => - Effect.gen(function* () { - const projectionPipeline = yield* OrchestrationProjectionPipeline; - const eventStore = yield* OrchestrationEventStore; - const sql = yield* SqlClient.SqlClient; - const now = new Date().toISOString(); - const later = new Date(Date.now() + 1_000).toISOString(); - - yield* eventStore.append({ - type: "project.created", - eventId: EventId.makeUnsafe("evt-overwrite-1"), - aggregateKind: "project", - aggregateId: ProjectId.makeUnsafe("project-overwrite"), - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-overwrite-1"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-overwrite-1"), - metadata: {}, - payload: { - projectId: ProjectId.makeUnsafe("project-overwrite"), - title: "Project Overwrite", - workspaceRoot: "/tmp/project-overwrite", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); + it.effect("overwrites stored attachment references when a message updates attachments", () => + Effect.sync(() => + fs.mkdtempSync(path.join(os.tmpdir(), "t3-projection-attachments-overwrite-")), + ).pipe( + Effect.flatMap((stateDir) => + Effect.gen(function* () { + const projectionPipeline = yield* OrchestrationProjectionPipeline; + const eventStore = yield* OrchestrationEventStore; + const sql = yield* SqlClient.SqlClient; + const now = new Date().toISOString(); + const later = new Date(Date.now() + 1_000).toISOString(); - yield* eventStore.append({ - type: "thread.created", - eventId: EventId.makeUnsafe("evt-overwrite-2"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-overwrite"), - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-overwrite-2"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-overwrite-2"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-overwrite"), - projectId: ProjectId.makeUnsafe("project-overwrite"), - title: "Thread Overwrite", - model: "gpt-5-codex", - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); + yield* eventStore.append({ + type: "project.created", + eventId: EventId.makeUnsafe("evt-overwrite-1"), + aggregateKind: "project", + aggregateId: ProjectId.makeUnsafe("project-overwrite"), + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-overwrite-1"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-overwrite-1"), + metadata: {}, + payload: { + projectId: ProjectId.makeUnsafe("project-overwrite"), + title: "Project Overwrite", + workspaceRoot: "/tmp/project-overwrite", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.makeUnsafe("evt-overwrite-3"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-overwrite"), - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-overwrite-3"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-overwrite-3"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-overwrite"), - messageId: MessageId.makeUnsafe("message-overwrite"), - role: "user", - text: "first image", - attachments: [ - { - type: "image", - id: "thread-overwrite-att-1", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); + yield* eventStore.append({ + type: "thread.created", + eventId: EventId.makeUnsafe("evt-overwrite-2"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-overwrite"), + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-overwrite-2"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-overwrite-2"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-overwrite"), + projectId: ProjectId.makeUnsafe("project-overwrite"), + title: "Thread Overwrite", + model: "gpt-5-codex", + runtimeMode: "full-access", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }); - yield* eventStore.append({ - type: "thread.message-sent", - eventId: EventId.makeUnsafe("evt-overwrite-4"), - aggregateKind: "thread", - aggregateId: ThreadId.makeUnsafe("thread-overwrite"), - occurredAt: later, - commandId: CommandId.makeUnsafe("cmd-overwrite-4"), - causationEventId: null, - correlationId: CommandId.makeUnsafe("cmd-overwrite-4"), - metadata: {}, - payload: { - threadId: ThreadId.makeUnsafe("thread-overwrite"), - messageId: MessageId.makeUnsafe("message-overwrite"), - role: "user", - text: "", - attachments: [ - { - type: "image", - id: "thread-overwrite-att-2", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: later, - }, - }); + yield* eventStore.append({ + type: "thread.message-sent", + eventId: EventId.makeUnsafe("evt-overwrite-3"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-overwrite"), + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-overwrite-3"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-overwrite-3"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-overwrite"), + messageId: MessageId.makeUnsafe("message-overwrite"), + role: "user", + text: "first image", + attachments: [ + { + type: "image", + id: "thread-overwrite-att-1", + name: "file.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + turnId: null, + streaming: false, + createdAt: now, + updatedAt: now, + }, + }); - yield* projectionPipeline.bootstrap; + yield* eventStore.append({ + type: "thread.message-sent", + eventId: EventId.makeUnsafe("evt-overwrite-4"), + aggregateKind: "thread", + aggregateId: ThreadId.makeUnsafe("thread-overwrite"), + occurredAt: later, + commandId: CommandId.makeUnsafe("cmd-overwrite-4"), + causationEventId: null, + correlationId: CommandId.makeUnsafe("cmd-overwrite-4"), + metadata: {}, + payload: { + threadId: ThreadId.makeUnsafe("thread-overwrite"), + messageId: MessageId.makeUnsafe("message-overwrite"), + role: "user", + text: "", + attachments: [ + { + type: "image", + id: "thread-overwrite-att-2", + name: "file.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + turnId: null, + streaming: false, + createdAt: now, + updatedAt: later, + }, + }); + + yield* projectionPipeline.bootstrap; - const rows = yield* sql<{ - readonly attachmentsJson: string | null; - }>` + const rows = yield* sql<{ + readonly attachmentsJson: string | null; + }>` SELECT attachments_json AS "attachmentsJson" FROM projection_thread_messages WHERE message_id = 'message-overwrite' `; - assert.equal(rows.length, 1); - assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [ - { - type: "image", - id: "thread-overwrite-att-2", - name: "file.png", - mimeType: "image/png", - sizeBytes: 5, - }, - ]); - }).pipe( - (effect) => runWithProjectionPipelineLayer(stateDir, effect), - Effect.ensuring( - Effect.sync(() => fs.rmSync(stateDir, { recursive: true, force: true })), - ), - ), + assert.equal(rows.length, 1); + assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [ + { + type: "image", + id: "thread-overwrite-att-2", + name: "file.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ]); + }).pipe( + (effect) => runWithProjectionPipelineLayer(stateDir, effect), + Effect.ensuring(Effect.sync(() => fs.rmSync(stateDir, { recursive: true, force: true }))), ), ), + ), ); it.effect("does not persist attachment files when projector transaction rolls back", () => @@ -711,11 +705,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { `; assert.equal(rows[0]?.count ?? 0, 0); - const attachmentPath = path.join( - stateDir, - "attachments", - "thread-rollback-att-1.png", - ); + const attachmentPath = path.join(stateDir, "attachments", "thread-rollback-att-1.png"); assert.equal(fs.existsSync(attachmentPath), false); yield* sql`DROP TRIGGER IF EXISTS fail_thread_messages_projection_state_update`; }).pipe( @@ -747,189 +737,189 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); yield* appendAndProject({ - type: "project.created", - eventId: EventId.makeUnsafe("evt-revert-files-1"), - aggregateKind: "project", - aggregateId: ProjectId.makeUnsafe("project-revert-files"), - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-1"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-1"), - metadata: {}, - payload: { - projectId: ProjectId.makeUnsafe("project-revert-files"), - title: "Project Revert Files", - workspaceRoot: "/tmp/project-revert-files", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); + type: "project.created", + eventId: EventId.makeUnsafe("evt-revert-files-1"), + aggregateKind: "project", + aggregateId: ProjectId.makeUnsafe("project-revert-files"), + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-1"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-1"), + metadata: {}, + payload: { + projectId: ProjectId.makeUnsafe("project-revert-files"), + title: "Project Revert Files", + workspaceRoot: "/tmp/project-revert-files", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.makeUnsafe("evt-revert-files-2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-2"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-2"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.makeUnsafe("project-revert-files"), - title: "Thread Revert Files", - model: "gpt-5-codex", - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); + yield* appendAndProject({ + type: "thread.created", + eventId: EventId.makeUnsafe("evt-revert-files-2"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-2"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-2"), + metadata: {}, + payload: { + threadId, + projectId: ProjectId.makeUnsafe("project-revert-files"), + title: "Thread Revert Files", + model: "gpt-5-codex", + runtimeMode: "full-access", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.makeUnsafe("evt-revert-files-3"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-3"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-3"), - metadata: {}, - payload: { - threadId, - turnId: TurnId.makeUnsafe("turn-keep"), - checkpointTurnCount: 1, - checkpointRef: CheckpointRef.makeUnsafe("refs/t3/checkpoints/thread-revert-files/turn/1"), - status: "ready", - files: [], - assistantMessageId: MessageId.makeUnsafe("message-keep"), - completedAt: now, - }, - }); + yield* appendAndProject({ + type: "thread.turn-diff-completed", + eventId: EventId.makeUnsafe("evt-revert-files-3"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-3"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-3"), + metadata: {}, + payload: { + threadId, + turnId: TurnId.makeUnsafe("turn-keep"), + checkpointTurnCount: 1, + checkpointRef: CheckpointRef.makeUnsafe( + "refs/t3/checkpoints/thread-revert-files/turn/1", + ), + status: "ready", + files: [], + assistantMessageId: MessageId.makeUnsafe("message-keep"), + completedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.makeUnsafe("evt-revert-files-4"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-4"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-4"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.makeUnsafe("message-keep"), - role: "assistant", - text: "Keep", - attachments: [ - { - type: "image", - id: keepAttachmentId, - name: "keep.png", - mimeType: "image/png", - sizeBytes: 5, + yield* appendAndProject({ + type: "thread.message-sent", + eventId: EventId.makeUnsafe("evt-revert-files-4"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-4"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-4"), + metadata: {}, + payload: { + threadId, + messageId: MessageId.makeUnsafe("message-keep"), + role: "assistant", + text: "Keep", + attachments: [ + { + type: "image", + id: keepAttachmentId, + name: "keep.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + turnId: TurnId.makeUnsafe("turn-keep"), + streaming: false, + createdAt: now, + updatedAt: now, }, - ], - turnId: TurnId.makeUnsafe("turn-keep"), - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); + }); - yield* appendAndProject({ - type: "thread.turn-diff-completed", - eventId: EventId.makeUnsafe("evt-revert-files-5"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-5"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-5"), - metadata: {}, - payload: { - threadId, - turnId: TurnId.makeUnsafe("turn-remove"), - checkpointTurnCount: 2, - checkpointRef: CheckpointRef.makeUnsafe("refs/t3/checkpoints/thread-revert-files/turn/2"), - status: "ready", - files: [], - assistantMessageId: MessageId.makeUnsafe("message-remove"), - completedAt: now, - }, - }); + yield* appendAndProject({ + type: "thread.turn-diff-completed", + eventId: EventId.makeUnsafe("evt-revert-files-5"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-5"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-5"), + metadata: {}, + payload: { + threadId, + turnId: TurnId.makeUnsafe("turn-remove"), + checkpointTurnCount: 2, + checkpointRef: CheckpointRef.makeUnsafe( + "refs/t3/checkpoints/thread-revert-files/turn/2", + ), + status: "ready", + files: [], + assistantMessageId: MessageId.makeUnsafe("message-remove"), + completedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.makeUnsafe("evt-revert-files-6"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-6"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-6"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.makeUnsafe("message-remove"), - role: "assistant", - text: "Remove", - attachments: [ - { - type: "image", - id: removeAttachmentId, - name: "remove.png", - mimeType: "image/png", - sizeBytes: 5, + yield* appendAndProject({ + type: "thread.message-sent", + eventId: EventId.makeUnsafe("evt-revert-files-6"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-6"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-6"), + metadata: {}, + payload: { + threadId, + messageId: MessageId.makeUnsafe("message-remove"), + role: "assistant", + text: "Remove", + attachments: [ + { + type: "image", + id: removeAttachmentId, + name: "remove.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + turnId: TurnId.makeUnsafe("turn-remove"), + streaming: false, + createdAt: now, + updatedAt: now, }, - ], - turnId: TurnId.makeUnsafe("turn-remove"), - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); + }); - const keepPath = path.join( - stateDir, - "attachments", - `${keepAttachmentId}.png`, - ); - const removePath = path.join( - stateDir, - "attachments", - `${removeAttachmentId}.png`, - ); - fs.mkdirSync(path.join(stateDir, "attachments"), { recursive: true }); - fs.writeFileSync(keepPath, Buffer.from("keep")); - fs.writeFileSync(removePath, Buffer.from("remove")); - const otherThreadPath = path.join(stateDir, "attachments", `${otherThreadAttachmentId}.png`); - fs.writeFileSync(otherThreadPath, Buffer.from("other")); - assert.equal(fs.existsSync(keepPath), true); - assert.equal(fs.existsSync(removePath), true); - assert.equal(fs.existsSync(otherThreadPath), true); + const keepPath = path.join(stateDir, "attachments", `${keepAttachmentId}.png`); + const removePath = path.join(stateDir, "attachments", `${removeAttachmentId}.png`); + fs.mkdirSync(path.join(stateDir, "attachments"), { recursive: true }); + fs.writeFileSync(keepPath, Buffer.from("keep")); + fs.writeFileSync(removePath, Buffer.from("remove")); + const otherThreadPath = path.join( + stateDir, + "attachments", + `${otherThreadAttachmentId}.png`, + ); + fs.writeFileSync(otherThreadPath, Buffer.from("other")); + assert.equal(fs.existsSync(keepPath), true); + assert.equal(fs.existsSync(removePath), true); + assert.equal(fs.existsSync(otherThreadPath), true); - yield* appendAndProject({ - type: "thread.reverted", - eventId: EventId.makeUnsafe("evt-revert-files-7"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-revert-files-7"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-revert-files-7"), - metadata: {}, - payload: { - threadId, - turnCount: 1, - }, - }); + yield* appendAndProject({ + type: "thread.reverted", + eventId: EventId.makeUnsafe("evt-revert-files-7"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-revert-files-7"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-revert-files-7"), + metadata: {}, + payload: { + threadId, + turnCount: 1, + }, + }); assert.equal(fs.existsSync(keepPath), true); assert.equal(fs.existsSync(removePath), false); @@ -962,107 +952,107 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); yield* appendAndProject({ - type: "project.created", - eventId: EventId.makeUnsafe("evt-delete-files-1"), - aggregateKind: "project", - aggregateId: ProjectId.makeUnsafe("project-delete-files"), - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-delete-files-1"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-delete-files-1"), - metadata: {}, - payload: { - projectId: ProjectId.makeUnsafe("project-delete-files"), - title: "Project Delete Files", - workspaceRoot: "/tmp/project-delete-files", - defaultModel: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }); + type: "project.created", + eventId: EventId.makeUnsafe("evt-delete-files-1"), + aggregateKind: "project", + aggregateId: ProjectId.makeUnsafe("project-delete-files"), + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-delete-files-1"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-delete-files-1"), + metadata: {}, + payload: { + projectId: ProjectId.makeUnsafe("project-delete-files"), + title: "Project Delete Files", + workspaceRoot: "/tmp/project-delete-files", + defaultModel: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.created", - eventId: EventId.makeUnsafe("evt-delete-files-2"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-delete-files-2"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-delete-files-2"), - metadata: {}, - payload: { - threadId, - projectId: ProjectId.makeUnsafe("project-delete-files"), - title: "Thread Delete Files", - model: "gpt-5-codex", - runtimeMode: "full-access", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, - }, - }); + yield* appendAndProject({ + type: "thread.created", + eventId: EventId.makeUnsafe("evt-delete-files-2"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-delete-files-2"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-delete-files-2"), + metadata: {}, + payload: { + threadId, + projectId: ProjectId.makeUnsafe("project-delete-files"), + title: "Thread Delete Files", + model: "gpt-5-codex", + runtimeMode: "full-access", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }); - yield* appendAndProject({ - type: "thread.message-sent", - eventId: EventId.makeUnsafe("evt-delete-files-3"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-delete-files-3"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-delete-files-3"), - metadata: {}, - payload: { - threadId, - messageId: MessageId.makeUnsafe("message-delete-files"), - role: "user", - text: "Delete", - attachments: [ - { - type: "image", - id: attachmentId, - name: "delete.png", - mimeType: "image/png", - sizeBytes: 5, + yield* appendAndProject({ + type: "thread.message-sent", + eventId: EventId.makeUnsafe("evt-delete-files-3"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-delete-files-3"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-delete-files-3"), + metadata: {}, + payload: { + threadId, + messageId: MessageId.makeUnsafe("message-delete-files"), + role: "user", + text: "Delete", + attachments: [ + { + type: "image", + id: attachmentId, + name: "delete.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + turnId: null, + streaming: false, + createdAt: now, + updatedAt: now, }, - ], - turnId: null, - streaming: false, - createdAt: now, - updatedAt: now, - }, - }); + }); - const threadAttachmentPath = path.join(stateDir, "attachments", `${attachmentId}.png`); - const otherThreadAttachmentPath = path.join( - stateDir, - "attachments", - `${otherThreadAttachmentId}.png`, - ); - fs.mkdirSync(path.join(stateDir, "attachments"), { recursive: true }); - fs.writeFileSync(threadAttachmentPath, Buffer.from("delete")); - fs.writeFileSync(otherThreadAttachmentPath, Buffer.from("other-thread")); - assert.equal(fs.existsSync(threadAttachmentPath), true); - assert.equal(fs.existsSync(otherThreadAttachmentPath), true); + const threadAttachmentPath = path.join(stateDir, "attachments", `${attachmentId}.png`); + const otherThreadAttachmentPath = path.join( + stateDir, + "attachments", + `${otherThreadAttachmentId}.png`, + ); + fs.mkdirSync(path.join(stateDir, "attachments"), { recursive: true }); + fs.writeFileSync(threadAttachmentPath, Buffer.from("delete")); + fs.writeFileSync(otherThreadAttachmentPath, Buffer.from("other-thread")); + assert.equal(fs.existsSync(threadAttachmentPath), true); + assert.equal(fs.existsSync(otherThreadAttachmentPath), true); - yield* appendAndProject({ - type: "thread.deleted", - eventId: EventId.makeUnsafe("evt-delete-files-4"), - aggregateKind: "thread", - aggregateId: threadId, - occurredAt: now, - commandId: CommandId.makeUnsafe("cmd-delete-files-4"), - causationEventId: null, - correlationId: CorrelationId.makeUnsafe("cmd-delete-files-4"), - metadata: {}, - payload: { - threadId, - deletedAt: now, - }, - }); + yield* appendAndProject({ + type: "thread.deleted", + eventId: EventId.makeUnsafe("evt-delete-files-4"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: CommandId.makeUnsafe("cmd-delete-files-4"), + causationEventId: null, + correlationId: CorrelationId.makeUnsafe("cmd-delete-files-4"), + metadata: {}, + payload: { + threadId, + deletedAt: now, + }, + }); assert.equal(fs.existsSync(threadAttachmentPath), false); assert.equal(fs.existsSync(otherThreadAttachmentPath), true); @@ -1075,7 +1065,9 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { ); it.effect("ignores unsafe thread ids for attachment cleanup paths", () => - Effect.sync(() => fs.mkdtempSync(path.join(os.tmpdir(), "t3-projection-attachments-unsafe-"))).pipe( + Effect.sync(() => + fs.mkdtempSync(path.join(os.tmpdir(), "t3-projection-attachments-unsafe-")), + ).pipe( Effect.flatMap((stateDir) => Effect.gen(function* () { const projectionPipeline = yield* OrchestrationProjectionPipeline; diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 24b81d514ab..6ae94105a67 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -78,9 +78,8 @@ interface AttachmentSideEffects { } const materializeAttachmentsForProjection = Effect.fn( - (input: { - readonly attachments: ReadonlyArray; - }) => Effect.succeed(input.attachments.length === 0 ? [] : input.attachments), + (input: { readonly attachments: ReadonlyArray }) => + Effect.succeed(input.attachments.length === 0 ? [] : input.attachments), ); function extractActivityRequestId(payload: unknown): ApprovalRequestId | null { @@ -336,7 +335,6 @@ const runAttachmentSideEffects = Effect.fn(function* (sideEffects: AttachmentSid }, { concurrency: 1 }, ); - }); const makeOrchestrationProjectionPipeline = Effect.gen(function* () { diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index e7e9cd4e127..fc7db548022 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -1,11 +1,4 @@ -import { - CheckpointRef, - EventId, - MessageId, - ProjectId, - ThreadId, - TurnId, -} from "@t3tools/contracts"; +import { CheckpointRef, EventId, MessageId, ProjectId, ThreadId, TurnId } from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; import { Effect, Layer } from "effect"; import * as SqlClient from "effect/unstable/sql/SqlClient"; diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index 4f352435fe5..cd74642b84f 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -41,7 +41,10 @@ const asApprovalRequestId = (value: string): ApprovalRequestId => const asMessageId = (value: string): MessageId => MessageId.makeUnsafe(value); const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); -async function waitFor(predicate: () => boolean | Promise, timeoutMs = 2000): Promise { +async function waitFor( + predicate: () => boolean | Promise, + timeoutMs = 2000, +): Promise { const deadline = Date.now() + timeoutMs; const poll = async (): Promise => { if (await predicate()) { @@ -101,7 +104,10 @@ describe("ProviderCommandReactor", () => { ? input.resumeCursor : undefined; const model = - typeof input === "object" && input !== null && "model" in input && typeof input.model === "string" + typeof input === "object" && + input !== null && + "model" in input && + typeof input.model === "string" ? input.model : undefined; const threadId = @@ -474,7 +480,9 @@ describe("ProviderCommandReactor", () => { await waitFor(async () => { const readModel = await Effect.runPromise(harness.engine.getReadModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + const thread = readModel.threads.find( + (entry) => entry.id === ThreadId.makeUnsafe("thread-1"), + ); return thread?.runtimeMode === "approval-required"; }); await waitFor(() => harness.startSession.mock.calls.length === 2); @@ -563,7 +571,9 @@ describe("ProviderCommandReactor", () => { await waitFor(async () => { const readModel = await Effect.runPromise(harness.engine.getReadModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + const thread = readModel.threads.find( + (entry) => entry.id === ThreadId.makeUnsafe("thread-1"), + ); return thread?.runtimeMode === "approval-required"; }); await waitFor(() => harness.startSession.mock.calls.length === 2); @@ -767,9 +777,13 @@ describe("ProviderCommandReactor", () => { await waitFor(async () => { const readModel = await Effect.runPromise(harness.engine.getReadModel()); - const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + const thread = readModel.threads.find( + (entry) => entry.id === ThreadId.makeUnsafe("thread-1"), + ); if (!thread) return false; - return thread.activities.some((activity) => activity.kind === "provider.approval.respond.failed"); + return thread.activities.some( + (activity) => activity.kind === "provider.approval.respond.failed", + ); }); const readModel = await Effect.runPromise(harness.engine.getReadModel()); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 2a72d590275..846eddf935f 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -226,9 +226,9 @@ const make = Effect.gen(function* () { }); const resolveActiveSession = (threadId: ThreadId) => - providerService.listSessions().pipe( - Effect.map((sessions) => sessions.find((session) => session.threadId === threadId)), - ); + providerService + .listSessions() + .pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId))); const startProviderSession = (input?: { readonly resumeCursor?: unknown; @@ -236,14 +236,16 @@ const make = Effect.gen(function* () { }) => providerService.startSession(threadId, { threadId, - ...(input?.provider ?? preferredProvider + ...((input?.provider ?? preferredProvider) ? { provider: input?.provider ?? preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), ...(desiredModel ? { model: desiredModel } : {}), ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}), ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}), - ...(options?.providerOptions !== undefined ? { providerOptions: options.providerOptions } : {}), + ...(options?.providerOptions !== undefined + ? { providerOptions: options.providerOptions } + : {}), ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), runtimeMode: desiredRuntimeMode, }); @@ -268,16 +270,15 @@ const make = Effect.gen(function* () { thread.session && thread.session.status !== "stopped" ? thread.id : null; if (existingSessionThreadId) { const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; - const providerChanged = options?.provider !== undefined && options.provider !== currentProvider; + const providerChanged = + options?.provider !== undefined && options.provider !== currentProvider; const activeSession = yield* resolveActiveSession(existingSessionThreadId); const sessionModelSwitch = currentProvider === undefined ? "in-session" : (yield* providerService.getCapabilities(currentProvider)).sessionModelSwitch; - const modelChanged = - options?.model !== undefined && options.model !== activeSession?.model; - const shouldRestartForModelChange = - modelChanged && sessionModelSwitch === "restart-session"; + const modelChanged = options?.model !== undefined && options.model !== activeSession?.model; + const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "restart-session"; if (!runtimeModeChanged && !providerChanged && !shouldRestartForModelChange) { return existingSessionThreadId; @@ -350,15 +351,16 @@ const make = Effect.gen(function* () { }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; - const activeSession = yield* providerService.listSessions().pipe( - Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), - ); + const activeSession = yield* providerService + .listSessions() + .pipe( + Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), + ); const sessionModelSwitch = activeSession === undefined ? "in-session" : (yield* providerService.getCapabilities(activeSession.provider)).sessionModelSwitch; - const modelForTurn = - sessionModelSwitch === "unsupported" ? activeSession?.model : input.model; + const modelForTurn = sessionModelSwitch === "unsupported" ? activeSession?.model : input.model; yield* providerService.sendTurn({ threadId: input.threadId, @@ -480,9 +482,15 @@ const make = Effect.gen(function* () { ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), ...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}), ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), - ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}), - ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}), - ...(event.payload.providerOptions !== undefined ? { providerOptions: event.payload.providerOptions } : {}), + ...(event.payload.serviceTier !== undefined + ? { serviceTier: event.payload.serviceTier } + : {}), + ...(event.payload.modelOptions !== undefined + ? { modelOptions: event.payload.modelOptions } + : {}), + ...(event.payload.providerOptions !== undefined + ? { providerOptions: event.payload.providerOptions } + : {}), interactionMode: event.payload.interactionMode, createdAt: event.payload.createdAt, }); @@ -640,7 +648,9 @@ const make = Effect.gen(function* () { } const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId); yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, { - ...(cachedProviderOptions !== undefined ? { providerOptions: cachedProviderOptions } : {}), + ...(cachedProviderOptions !== undefined + ? { providerOptions: cachedProviderOptions } + : {}), }); return; } diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 24409655eb8..4483170c9d7 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -379,7 +379,9 @@ describe("ProviderRuntimeIngestion", () => { await Effect.runPromise(Effect.sleep("40 millis")); const midReadModel = await Effect.runPromise(harness.engine.getReadModel()); - const midThread = midReadModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1")); + const midThread = midReadModel.threads.find( + (entry) => entry.id === ThreadId.makeUnsafe("thread-1"), + ); expect(midThread?.session?.status).toBe("running"); expect(midThread?.session?.activeTurnId).toBe("turn-midturn-lifecycle"); @@ -620,7 +622,9 @@ describe("ProviderRuntimeIngestion", () => { const proposedPlan = thread.proposedPlans.find( (entry: ProviderRuntimeTestProposedPlan) => entry.id === "plan:thread-1:turn:turn-plan-final", ); - expect(proposedPlan?.planMarkdown).toBe("## Ship plan\n\n- wire projection\n- render follow-up"); + expect(proposedPlan?.planMarkdown).toBe( + "## Ship plan\n\n- wire projection\n- render follow-up", + ); }); it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => { @@ -683,7 +687,8 @@ describe("ProviderRuntimeIngestion", () => { ), ); const proposedPlan = thread.proposedPlans.find( - (entry: ProviderRuntimeTestProposedPlan) => entry.id === "plan:thread-1:turn:turn-plan-buffer", + (entry: ProviderRuntimeTestProposedPlan) => + entry.id === "plan:thread-1:turn:turn-plan-buffer", ); expect(proposedPlan?.planMarkdown).toBe("## Buffered plan\n\n- first\n- second"); }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 5a6f71d5e6a..bea3cdc8130 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -210,9 +210,9 @@ function runtimeEventToActivities( ? "Command approval requested" : requestKind === "file-read" ? "File-read approval requested" - : requestKind === "file-change" - ? "File-change approval requested" - : "Approval requested", + : requestKind === "file-change" + ? "File-change approval requested" + : "Approval requested", payload: { requestId: toApprovalRequestId(event.requestId), ...(requestKind ? { requestKind } : {}), @@ -298,7 +298,9 @@ function runtimeEventToActivities( summary: "Plan updated", payload: { plan: event.payload.plan, - ...(event.payload.explanation !== undefined ? { explanation: event.payload.explanation } : {}), + ...(event.payload.explanation !== undefined + ? { explanation: event.payload.explanation } + : {}), }, turnId: toTurnId(event.turnId) ?? null, ...maybeSequence, @@ -358,7 +360,9 @@ function runtimeEventToActivities( payload: { taskId: event.payload.taskId, ...(event.payload.taskType ? { taskType: event.payload.taskType } : {}), - ...(event.payload.description ? { detail: truncateDetail(event.payload.description) } : {}), + ...(event.payload.description + ? { detail: truncateDetail(event.payload.description) } + : {}), }, turnId: toTurnId(event.turnId) ?? null, ...maybeSequence, @@ -525,11 +529,7 @@ const make = Effect.gen(function* () { return isGitRepository(workspaceCwd); }); - const rememberAssistantMessageId = ( - threadId: ThreadId, - turnId: TurnId, - messageId: MessageId, - ) => + const rememberAssistantMessageId = (threadId: ThreadId, turnId: TurnId, messageId: MessageId) => Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( Effect.flatMap((existingIds) => Cache.set( @@ -547,11 +547,7 @@ const make = Effect.gen(function* () { ), ); - const forgetAssistantMessageId = ( - threadId: ThreadId, - turnId: TurnId, - messageId: MessageId, - ) => + const forgetAssistantMessageId = (threadId: ThreadId, turnId: TurnId, messageId: MessageId) => Cache.getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe( Effect.flatMap((existingIds) => Option.match(existingIds, { @@ -616,7 +612,8 @@ const make = Effect.gen(function* () { const existing = Option.getOrUndefined(existingEntry); return Cache.set(bufferedProposedPlanById, planId, { text: `${existing?.text ?? ""}${delta}`, - createdAt: existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt, + createdAt: + existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt, }); }), ); @@ -633,7 +630,8 @@ const make = Effect.gen(function* () { const clearBufferedProposedPlan = (planId: string) => Cache.invalidate(bufferedProposedPlanById, planId); - const clearAssistantMessageState = (messageId: MessageId) => clearBufferedAssistantText(messageId); + const clearAssistantMessageState = (messageId: MessageId) => + clearBufferedAssistantText(messageId); const finalizeAssistantMessage = (input: { event: ProviderRuntimeEvent; @@ -862,8 +860,8 @@ const make = Effect.gen(function* () { : event.type === "turn.completed" && runtimeTurnState(event) === "failed" ? (runtimeTurnErrorMessage(event) ?? thread.session?.lastError ?? "Turn failed") : status === "ready" - ? null - : (thread.session?.lastError ?? null); + ? null + : (thread.session?.lastError ?? null); if (shouldApplyThreadLifecycle) { yield* orchestrationEngine.dispatch({ @@ -935,7 +933,9 @@ const make = Effect.gen(function* () { const assistantCompletion = event.type === "item.completed" && event.payload.itemType === "assistant_message" ? { - messageId: MessageId.makeUnsafe(`assistant:${event.itemId ?? event.turnId ?? event.eventId}`), + messageId: MessageId.makeUnsafe( + `assistant:${event.itemId ?? event.turnId ?? event.eventId}`, + ), fallbackText: event.payload.detail, } : undefined; @@ -951,10 +951,11 @@ const make = Effect.gen(function* () { if (assistantCompletion) { const assistantMessageId = assistantCompletion.messageId; const turnId = toTurnId(event.turnId); - const existingAssistantMessage = thread.messages.find((entry) => entry.id === assistantMessageId); + const existingAssistantMessage = thread.messages.find( + (entry) => entry.id === assistantMessageId, + ); const shouldApplyFallbackCompletionText = - !existingAssistantMessage || - existingAssistantMessage.text.length === 0; + !existingAssistantMessage || existingAssistantMessage.text.length === 0; if (turnId) { yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId); } @@ -1029,9 +1030,7 @@ const make = Effect.gen(function* () { const shouldApplyRuntimeError = !STRICT_PROVIDER_LIFECYCLE_GUARD ? true - : activeTurnId === null || - eventTurnId === undefined || - sameId(activeTurnId, eventTurnId); + : activeTurnId === null || eventTurnId === undefined || sameId(activeTurnId, eventTurnId); if (shouldApplyRuntimeError) { yield* orchestrationEngine.dispatch({ diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 1c1ca4dd795..47bfc3d927c 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -303,7 +303,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}), ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}), - ...(command.providerOptions !== undefined ? { providerOptions: command.providerOptions } : {}), + ...(command.providerOptions !== undefined + ? { providerOptions: command.providerOptions } + : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, runtimeMode: readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ?? diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index c0badfe9588..60575d30ae9 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -304,12 +304,7 @@ export function projectEvent( ); case "thread.runtime-mode-set": - return decodeForEvent( - ThreadRuntimeModeSetPayload, - event.payload, - event.type, - "payload", - ).pipe( + return decodeForEvent(ThreadRuntimeModeSetPayload, event.payload, event.type, "payload").pipe( Effect.map((payload) => ({ ...nextBase, threads: updateThread(nextBase.threads, payload.threadId, { diff --git a/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts b/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts index 24446e04dc3..3d103592f95 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts @@ -70,9 +70,7 @@ const makeProjectionThreadProposedPlanRepository = Effect.gen(function* () { const upsert: ProjectionThreadProposedPlanRepositoryShape["upsert"] = (row) => upsertProjectionThreadProposedPlanRow(row).pipe( - Effect.mapError( - toPersistenceSqlError("ProjectionThreadProposedPlanRepository.upsert:query"), - ), + Effect.mapError(toPersistenceSqlError("ProjectionThreadProposedPlanRepository.upsert:query")), ); const listByThreadId: ProjectionThreadProposedPlanRepositoryShape["listByThreadId"] = (input) => diff --git a/apps/server/src/projectFaviconRoute.test.ts b/apps/server/src/projectFaviconRoute.test.ts index 34a430d9742..a346e513ebf 100644 --- a/apps/server/src/projectFaviconRoute.test.ts +++ b/apps/server/src/projectFaviconRoute.test.ts @@ -20,9 +20,7 @@ function makeTempDir(prefix: string): string { return dir; } -async function withRouteServer( - run: (baseUrl: string) => Promise, -): Promise { +async function withRouteServer(run: (baseUrl: string) => Promise): Promise { const server = http.createServer((req, res) => { const url = new URL(req.url ?? "/", "http://127.0.0.1"); if (tryHandleProjectFaviconRequest(url, res)) { @@ -104,7 +102,10 @@ describe("tryHandleProjectFaviconRequest", () => { const projectDir = makeTempDir("t3code-favicon-route-source-"); const iconPath = path.join(projectDir, "public", "brand", "logo.svg"); fs.mkdirSync(path.dirname(iconPath), { recursive: true }); - fs.writeFileSync(path.join(projectDir, "index.html"), ''); + fs.writeFileSync( + path.join(projectDir, "index.html"), + '', + ); fs.writeFileSync(iconPath, "brand", "utf8"); await withRouteServer(async (baseUrl) => { @@ -120,7 +121,10 @@ describe("tryHandleProjectFaviconRequest", () => { const projectDir = makeTempDir("t3code-favicon-route-html-order-"); const iconPath = path.join(projectDir, "public", "brand", "logo.svg"); fs.mkdirSync(path.dirname(iconPath), { recursive: true }); - fs.writeFileSync(path.join(projectDir, "index.html"), ''); + fs.writeFileSync( + path.join(projectDir, "index.html"), + '', + ); fs.writeFileSync(iconPath, "brand-html-order", "utf8"); await withRouteServer(async (baseUrl) => { diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 9a6271b7923..1f14b648e1b 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -403,7 +403,9 @@ function asRuntimeTaskId(taskId: string): RuntimeTaskId { return RuntimeTaskId.makeUnsafe(taskId); } -function codexEventMessage(payload: Record | undefined): Record | undefined { +function codexEventMessage( + payload: Record | undefined, +): Record | undefined { return asObject(payload?.msg); } @@ -1037,7 +1039,9 @@ function mapToRuntimeEvents( type: "content.delta", payload: { streamKind: - asNumber(msg?.summary_index) !== undefined ? "reasoning_summary_text" : "reasoning_text", + asNumber(msg?.summary_index) !== undefined + ? "reasoning_summary_text" + : "reasoning_text", delta, ...(asNumber(msg?.summary_index) !== undefined ? { summaryIndex: asNumber(msg?.summary_index) } @@ -1313,9 +1317,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) => detail: toMessage(cause, "Failed to start Codex adapter session."), cause, }), - }).pipe( - Effect.map((session) => session), - ); + }).pipe(Effect.map((session) => session)); }; const sendTurn: CodexAdapterShape["sendTurn"] = (input) => diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts index a50112a62d8..c6f4a3c08cb 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts @@ -29,10 +29,7 @@ const fakeCodexAdapter: CodexAdapterShape = { const layer = it.layer( Layer.mergeAll( - Layer.provide( - ProviderAdapterRegistryLive, - Layer.succeed(CodexAdapter, fakeCodexAdapter), - ), + Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)), NodeServices.layer, ), ); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts index 4f2c7f2c7e0..3062ed79076 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts @@ -23,10 +23,7 @@ export interface ProviderAdapterRegistryLiveOptions { const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => Effect.gen(function* () { - const adapters = - options?.adapters !== undefined - ? options.adapters - : [yield* CodexAdapter]; + const adapters = options?.adapters !== undefined ? options.adapters : [yield* CodexAdapter]; const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 90df9b691fe..964b45ba49b 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -132,30 +132,27 @@ it.effect("returns unauthenticated when auth probe reports login required", () = ), ); -it.effect( - "returns unauthenticated when login status output includes 'not logged in'", - () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "unauthenticated"); - assert.strictEqual( - status.message, - "Codex CLI is not authenticated. Run `codex login` and try again.", - ); - }).pipe( - Effect.provide( - mockSpawnerLayer((args) => { - const joined = args.join(" "); - if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; - if (joined === "login status") - return { stdout: "Not logged in\n", stderr: "", code: 1 }; - throw new Error(`Unexpected args: ${joined}`); - }), - ), +it.effect("returns unauthenticated when login status output includes 'not logged in'", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "unauthenticated"); + assert.strictEqual( + status.message, + "Codex CLI is not authenticated. Run `codex login` and try again.", + ); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; + if (joined === "login status") return { stdout: "Not logged in\n", stderr: "", code: 1 }; + throw new Error(`Unexpected args: ${joined}`); + }), ), + ), ); it.effect("returns warning when login status command is unsupported", () => diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 63b41d6b065..178f86916e3 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -635,7 +635,6 @@ routing.layer("ProviderServiceLive routing", (it) => { assert.equal(runtimePayload.lastRuntimeEvent, "provider.sendTurn"); } } - }), ); }); diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts index 05a1de14952..088f5ba5f1a 100644 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ b/apps/server/src/provider/Layers/ProviderService.ts @@ -141,9 +141,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => Effect.succeed(event).pipe( Effect.tap((canonicalEvent) => - canonicalEventLogger - ? canonicalEventLogger.write(canonicalEvent, null) - : Effect.void, + canonicalEventLogger ? canonicalEventLogger.write(canonicalEvent, null) : Effect.void, ), Effect.flatMap((canonicalEvent) => PubSub.publish(runtimeEventPubSub, canonicalEvent)), Effect.asVoid, @@ -193,7 +191,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => const hasActiveSession = yield* adapter.hasSession(input.binding.threadId); if (hasActiveSession) { const activeSessions = yield* adapter.listSessions(); - const existing = activeSessions.find((session) => session.threadId === input.binding.threadId); + const existing = activeSessions.find( + (session) => session.threadId === input.binding.threadId, + ); if (existing) { yield* upsertSessionBinding(existing, input.binding.threadId); yield* analytics.record("provider.session.recovered", { @@ -292,7 +292,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => } yield* upsertSessionBinding(session, threadId, { - ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), + ...(input.providerOptions !== undefined + ? { providerOptions: input.providerOptions } + : {}), }); yield* analytics.record("provider.session.started", { provider: session.provider, @@ -425,23 +427,23 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => const listSessions: ProviderServiceShape["listSessions"] = () => Effect.gen(function* () { - const sessionsByProvider = yield* Effect.forEach(adapters, (adapter) => adapter.listSessions()); + const sessionsByProvider = yield* Effect.forEach(adapters, (adapter) => + adapter.listSessions(), + ); const activeSessions = sessionsByProvider.flatMap((sessions) => sessions); - const persistedBindings = yield* directory - .listThreadIds() - .pipe( - Effect.flatMap((threadIds) => - Effect.forEach( - threadIds, - (threadId) => - directory.getBinding(threadId).pipe( - Effect.orElseSucceed(() => Option.none()), - ), - { concurrency: "unbounded" }, - ), + const persistedBindings = yield* directory.listThreadIds().pipe( + Effect.flatMap((threadIds) => + Effect.forEach( + threadIds, + (threadId) => + directory + .getBinding(threadId) + .pipe(Effect.orElseSucceed(() => Option.none())), + { concurrency: "unbounded" }, ), - Effect.orElseSucceed(() => [] as Array>), - ); + ), + Effect.orElseSucceed(() => [] as Array>), + ); const bindingsByThreadId = new Map(); for (const bindingOption of persistedBindings) { const binding = Option.getOrUndefined(bindingOption); diff --git a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts index 69e1e439bfd..38e097e1c9e 100644 --- a/apps/server/src/provider/Layers/ProviderSessionDirectory.ts +++ b/apps/server/src/provider/Layers/ProviderSessionDirectory.ts @@ -2,10 +2,7 @@ import { type ProviderKind, type ThreadId } from "@t3tools/contracts"; import { Effect, Layer, Option } from "effect"; import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; -import { - ProviderSessionDirectoryPersistenceError, - ProviderValidationError, -} from "../Errors.ts"; +import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts"; import { ProviderSessionDirectory, type ProviderRuntimeBinding, @@ -138,7 +135,9 @@ const makeProviderSessionDirectory = Effect.gen(function* () { const remove: ProviderSessionDirectoryShape["remove"] = (threadId) => repository .deleteByThreadId({ threadId }) - .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteByThreadId"))); + .pipe( + Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteByThreadId")), + ); const listThreadIds: ProviderSessionDirectoryShape["listThreadIds"] = () => repository.list().pipe( diff --git a/apps/server/src/provider/Services/ProviderAdapter.ts b/apps/server/src/provider/Services/ProviderAdapter.ts index 67755b53834..38a05f75748 100644 --- a/apps/server/src/provider/Services/ProviderAdapter.ts +++ b/apps/server/src/provider/Services/ProviderAdapter.ts @@ -66,10 +66,7 @@ export interface ProviderAdapterShape { /** * Interrupt an active turn. */ - readonly interruptTurn: ( - threadId: ThreadId, - turnId?: TurnId, - ) => Effect.Effect; + readonly interruptTurn: (threadId: ThreadId, turnId?: TurnId) => Effect.Effect; /** * Respond to an interactive approval request. @@ -107,9 +104,7 @@ export interface ProviderAdapterShape { /** * Read a provider thread snapshot. */ - readonly readThread: ( - threadId: ThreadId, - ) => Effect.Effect; + readonly readThread: (threadId: ThreadId) => Effect.Effect; /** * Roll back a provider thread by N turns. diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index 764e455f550..71728ca7fd4 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -1236,21 +1236,19 @@ describe("WebSocket Server", () => { return event.type === "thread.session-set"; }); - emitRuntimeEvent( - { - type: "content.delta", - eventId: asEventId("evt-ws-runtime-message-delta"), - provider: "codex", - threadId: asThreadId("thread-1"), - createdAt: new Date().toISOString(), - turnId: asTurnId("turn-1"), - itemId: asProviderItemId("item-1"), - payload: { - streamKind: "assistant_text", - delta: "hello from runtime", - }, - } as unknown as ProviderRuntimeEvent, - ); + emitRuntimeEvent({ + type: "content.delta", + eventId: asEventId("evt-ws-runtime-message-delta"), + provider: "codex", + threadId: asThreadId("thread-1"), + createdAt: new Date().toISOString(), + turnId: asTurnId("turn-1"), + itemId: asProviderItemId("item-1"), + payload: { + streamKind: "assistant_text", + delta: "hello from runtime", + }, + } as unknown as ProviderRuntimeEvent); const domainPush = await waitForPush(ws, ORCHESTRATION_WS_CHANNELS.domainEvent, (push) => { const event = push.data as { type?: string; payload?: { messageId?: string; text?: string } }; @@ -1568,7 +1566,9 @@ describe("WebSocket Server", () => { }); expect(response.result).toBeUndefined(); - expect(response.error?.message).toContain("Workspace file path must stay within the project root."); + expect(response.error?.message).toContain( + "Workspace file path must stay within the project root.", + ); expect(fs.existsSync(path.join(workspace, "..", "escape.md"))).toBe(false); }); diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index d8859c2fa5b..a03ceb0e828 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -197,8 +197,7 @@ function stripRequestTag(body: T) { function messageFromCause(cause: Cause.Cause): string { const squashed = Cause.squash(cause); - const message = - squashed instanceof Error ? squashed.message.trim() : String(squashed).trim(); + const message = squashed instanceof Error ? squashed.message.trim() : String(squashed).trim(); return message.length > 0 ? message : Cause.pretty(cause); } @@ -331,10 +330,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< } satisfies OrchestrationCommand; } - if ( - input.command.type === "project.meta.update" && - input.command.workspaceRoot !== undefined - ) { + if (input.command.type === "project.meta.update" && input.command.workspaceRoot !== undefined) { return { ...input.command, workspaceRoot: yield* normalizeProjectWorkspaceRoot(input.command.workspaceRoot), @@ -777,14 +773,16 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< relativePath: body.relativePath, path, }); - yield* fileSystem.makeDirectory(path.dirname(target.absolutePath), { recursive: true }).pipe( - Effect.mapError( - (cause) => - new RouteRequestError({ - message: `Failed to prepare workspace path: ${String(cause)}`, - }), - ), - ); + yield* fileSystem + .makeDirectory(path.dirname(target.absolutePath), { recursive: true }) + .pipe( + Effect.mapError( + (cause) => + new RouteRequestError({ + message: `Failed to prepare workspace path: ${String(cause)}`, + }), + ), + ); yield* fileSystem.writeFileString(target.absolutePath, body.contents).pipe( Effect.mapError( (cause) => diff --git a/apps/web/public/mockServiceWorker.js b/apps/web/public/mockServiceWorker.js index daa58d0f120..82804cec786 100644 --- a/apps/web/public/mockServiceWorker.js +++ b/apps/web/public/mockServiceWorker.js @@ -7,114 +7,111 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.10' -const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' -const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') -const activeClientIds = new Set() +const PACKAGE_VERSION = "2.12.10"; +const INTEGRITY_CHECKSUM = "4db4a41e972cec1b64cc569c66952d82"; +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); +const activeClientIds = new Set(); -addEventListener('install', function () { - self.skipWaiting() -}) +addEventListener("install", function () { + self.skipWaiting(); +}); -addEventListener('activate', function (event) { - event.waitUntil(self.clients.claim()) -}) +addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); -addEventListener('message', async function (event) { - const clientId = Reflect.get(event.source || {}, 'id') +addEventListener("message", async function (event) { + const clientId = Reflect.get(event.source || {}, "id"); if (!clientId || !self.clients) { - return + return; } - const client = await self.clients.get(clientId) + const client = await self.clients.get(clientId); if (!client) { - return + return; } const allClients = await self.clients.matchAll({ - type: 'window', - }) + type: "window", + }); switch (event.data) { - case 'KEEPALIVE_REQUEST': { + case "KEEPALIVE_REQUEST": { sendToClient(client, { - type: 'KEEPALIVE_RESPONSE', - }) - break + type: "KEEPALIVE_RESPONSE", + }); + break; } - case 'INTEGRITY_CHECK_REQUEST': { + case "INTEGRITY_CHECK_REQUEST": { sendToClient(client, { - type: 'INTEGRITY_CHECK_RESPONSE', + type: "INTEGRITY_CHECK_RESPONSE", payload: { packageVersion: PACKAGE_VERSION, checksum: INTEGRITY_CHECKSUM, }, - }) - break + }); + break; } - case 'MOCK_ACTIVATE': { - activeClientIds.add(clientId) + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId); sendToClient(client, { - type: 'MOCKING_ENABLED', + type: "MOCKING_ENABLED", payload: { client: { id: client.id, frameType: client.frameType, }, }, - }) - break + }); + break; } - case 'CLIENT_CLOSED': { - activeClientIds.delete(clientId) + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId); const remainingClients = allClients.filter((client) => { - return client.id !== clientId - }) + return client.id !== clientId; + }); // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister() + self.registration.unregister(); } - break + break; } } -}) +}); -addEventListener('fetch', function (event) { - const requestInterceptedAt = Date.now() +addEventListener("fetch", function (event) { + const requestInterceptedAt = Date.now(); // Bypass navigation requests. - if (event.request.mode === 'navigate') { - return + if (event.request.mode === "navigate") { + return; } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if ( - event.request.cache === 'only-if-cached' && - event.request.mode !== 'same-origin' - ) { - return + if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { + return; } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been terminated (still remains active until the next reload). if (activeClientIds.size === 0) { - return + return; } - const requestId = crypto.randomUUID() - event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) -}) + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)); +}); /** * @param {FetchEvent} event @@ -122,28 +119,23 @@ addEventListener('fetch', function (event) { * @param {number} requestInterceptedAt */ async function handleRequest(event, requestId, requestInterceptedAt) { - const client = await resolveMainClient(event) - const requestCloneForEvents = event.request.clone() - const response = await getResponse( - event, - client, - requestId, - requestInterceptedAt, - ) + const client = await resolveMainClient(event); + const requestCloneForEvents = event.request.clone(); + const response = await getResponse(event, client, requestId, requestInterceptedAt); // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - const serializedRequest = await serializeRequest(requestCloneForEvents) + const serializedRequest = await serializeRequest(requestCloneForEvents); // Clone the response so both the client and the library could consume it. - const responseClone = response.clone() + const responseClone = response.clone(); sendToClient( client, { - type: 'RESPONSE', + type: "RESPONSE", payload: { isMockedResponse: IS_MOCKED_RESPONSE in response, request: { @@ -160,10 +152,10 @@ async function handleRequest(event, requestId, requestInterceptedAt) { }, }, responseClone.body ? [serializedRequest.body, responseClone.body] : [], - ) + ); } - return response + return response; } /** @@ -175,30 +167,30 @@ async function handleRequest(event, requestId, requestInterceptedAt) { * @returns {Promise} */ async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId) + const client = await self.clients.get(event.clientId); if (activeClientIds.has(event.clientId)) { - return client + return client; } - if (client?.frameType === 'top-level') { - return client + if (client?.frameType === "top-level") { + return client; } const allClients = await self.clients.matchAll({ - type: 'window', - }) + type: "window", + }); return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === 'visible' + return client.visibilityState === "visible"; }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id) - }) + return activeClientIds.has(client.id); + }); } /** @@ -211,36 +203,34 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId, requestInterceptedAt) { // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = event.request.clone() + const requestClone = event.request.clone(); function passthrough() { // Cast the request headers to a new Headers instance // so the headers can be manipulated with. - const headers = new Headers(requestClone.headers) + const headers = new Headers(requestClone.headers); // Remove the "accept" header value that marked this request as passthrough. // This prevents request alteration and also keeps it compliant with the // user-defined CORS policies. - const acceptHeader = headers.get('accept') + const acceptHeader = headers.get("accept"); if (acceptHeader) { - const values = acceptHeader.split(',').map((value) => value.trim()) - const filteredValues = values.filter( - (value) => value !== 'msw/passthrough', - ) + const values = acceptHeader.split(",").map((value) => value.trim()); + const filteredValues = values.filter((value) => value !== "msw/passthrough"); if (filteredValues.length > 0) { - headers.set('accept', filteredValues.join(', ')) + headers.set("accept", filteredValues.join(", ")); } else { - headers.delete('accept') + headers.delete("accept"); } } - return fetch(requestClone, { headers }) + return fetch(requestClone, { headers }); } // Bypass mocking when the client is not active. if (!client) { - return passthrough() + return passthrough(); } // Bypass initial page load requests (i.e. static assets). @@ -248,15 +238,15 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough() + return passthrough(); } // Notify the client that a request has been intercepted. - const serializedRequest = await serializeRequest(event.request) + const serializedRequest = await serializeRequest(event.request); const clientMessage = await sendToClient( client, { - type: 'REQUEST', + type: "REQUEST", payload: { id: requestId, interceptedAt: requestInterceptedAt, @@ -264,19 +254,19 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { }, }, [serializedRequest.body], - ) + ); switch (clientMessage.type) { - case 'MOCK_RESPONSE': { - return respondWithMock(clientMessage.data) + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data); } - case 'PASSTHROUGH': { - return passthrough() + case "PASSTHROUGH": { + return passthrough(); } } - return passthrough() + return passthrough(); } /** @@ -287,21 +277,18 @@ async function getResponse(event, client, requestId, requestInterceptedAt) { */ function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { - const channel = new MessageChannel() + const channel = new MessageChannel(); channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error) + return reject(event.data.error); } - resolve(event.data) - } + resolve(event.data); + }; - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]) - }) + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]); + }); } /** @@ -314,17 +301,17 @@ function respondWithMock(response) { // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { - return Response.error() + return Response.error(); } - const mockedResponse = new Response(response.body, response) + const mockedResponse = new Response(response.body, response); Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, - }) + }); - return mockedResponse + return mockedResponse; } /** @@ -345,5 +332,5 @@ async function serializeRequest(request) { referrerPolicy: request.referrerPolicy, body: await request.arrayBuffer(), keepalive: request.keepalive, - } + }; } diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index 5ab5d3c90ad..5bb7adf15aa 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -63,23 +63,13 @@ describe("resolveAppModelSelection", () => { describe("getSlashModelOptions", () => { it("includes saved custom model slugs for /model command suggestions", () => { - const options = getSlashModelOptions( - "codex", - ["custom/internal-model"], - "", - "gpt-5.3-codex", - ); + const options = getSlashModelOptions("codex", ["custom/internal-model"], "", "gpt-5.3-codex"); expect(options.some((option) => option.slug === "custom/internal-model")).toBe(true); }); it("filters slash-model suggestions across built-in and custom model names", () => { - const options = getSlashModelOptions( - "codex", - ["openai/gpt-oss-120b"], - "oss", - "gpt-5.3-codex", - ); + const options = getSlashModelOptions("codex", ["openai/gpt-oss-120b"], "oss", "gpt-5.3-codex"); expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); }); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index e58f7af4a67..466f5debdb9 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -41,7 +41,9 @@ const AppSettingsSchema = Schema.Struct({ enableAssistantStreaming: Schema.Boolean.pipe( Schema.withConstructorDefault(() => Option.some(false)), ), - codexServiceTier: AppServiceTierSchema.pipe(Schema.withConstructorDefault(() => Option.some("auto"))), + codexServiceTier: AppServiceTierSchema.pipe( + Schema.withConstructorDefault(() => Option.some("auto")), + ), customCodexModels: Schema.Array(Schema.String).pipe( Schema.withConstructorDefault(() => Option.some([])), ), diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 23abfffc154..8d16ab12a7e 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -360,9 +360,9 @@ async function setViewport(viewport: ViewportSpec): Promise { async function waitForProductionStyles(): Promise { await vi.waitFor( () => { - expect(getComputedStyle(document.documentElement).getPropertyValue("--background").trim()).not.toBe( - "", - ); + expect( + getComputedStyle(document.documentElement).getPropertyValue("--background").trim(), + ).not.toBe(""); expect(getComputedStyle(document.body).marginTop).toBe("0px"); }, { @@ -400,7 +400,9 @@ async function waitForComposerEditor(): Promise { ); } -async function waitForInteractionModeButton(expectedLabel: "Chat" | "Plan"): Promise { +async function waitForInteractionModeButton( + expectedLabel: "Chat" | "Plan", +): Promise { return waitForElement( () => Array.from(document.querySelectorAll("button")).find( @@ -643,7 +645,9 @@ describe("ChatView timeline estimator parity (full app)", () => { }); try { - const measurements: Array = []; + const measurements: Array< + UserRowMeasurement & { viewport: ViewportSpec; estimatedHeightPx: number } + > = []; for (const viewport of TEXT_VIEWPORT_MATRIX) { await mounted.setViewport(viewport); @@ -660,7 +664,10 @@ describe("ChatView timeline estimator parity (full app)", () => { measurements.push({ ...measurement, viewport, estimatedHeightPx }); } - expect(new Set(measurements.map((measurement) => Math.round(measurement.timelineWidthMeasuredPx))).size).toBeGreaterThanOrEqual(3); + expect( + new Set(measurements.map((measurement) => Math.round(measurement.timelineWidthMeasuredPx))) + .size, + ).toBeGreaterThanOrEqual(3); const byMeasuredWidth = measurements.toSorted( (left, right) => left.timelineWidthMeasuredPx - right.timelineWidthMeasuredPx, @@ -702,7 +709,8 @@ describe("ChatView timeline estimator parity (full app)", () => { { timelineWidthPx: mobileMeasurement.timelineWidthMeasuredPx }, ); - const measuredDeltaPx = mobileMeasurement.measuredRowHeightPx - desktopMeasurement.measuredRowHeightPx; + const measuredDeltaPx = + mobileMeasurement.measuredRowHeightPx - desktopMeasurement.measuredRowHeightPx; const estimatedDeltaPx = estimatedMobilePx - estimatedDesktopPx; expect(measuredDeltaPx).toBeGreaterThan(0); expect(estimatedDeltaPx).toBeGreaterThan(0); @@ -790,7 +798,9 @@ describe("ChatView timeline estimator parity (full app)", () => { await vi.waitFor( () => { - const openRequest = wsRequests.find((request) => request._tag === WS_METHODS.shellOpenInEditor); + const openRequest = wsRequests.find( + (request) => request._tag === WS_METHODS.shellOpenInEditor, + ); expect(openRequest).toMatchObject({ _tag: WS_METHODS.shellOpenInEditor, cwd: "/repo/project", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 51b300ba8ec..d683ea99077 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -295,8 +295,6 @@ function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { return "text-muted-foreground/40"; } - - interface ExpandedImageItem { src: string; name: string; @@ -1942,8 +1940,6 @@ export default function ChatView({ threadId }: ChatViewProps) { planSidebarDismissedForTurnRef.current = null; }, [activeThread?.id]); - - useEffect(() => { if (!composerMenuOpen) { setComposerHighlightedItemId(null); @@ -2668,9 +2664,7 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), - ...(providerOptionsForDispatch - ? { providerOptions: providerOptionsForDispatch } - : {}), + ...(providerOptionsForDispatch ? { providerOptions: providerOptionsForDispatch } : {}), provider: selectedProvider, assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, @@ -2948,9 +2942,7 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), - ...(providerOptionsForDispatch - ? { providerOptions: providerOptionsForDispatch } - : {}), + ...(providerOptionsForDispatch ? { providerOptions: providerOptionsForDispatch } : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: nextInteractionMode, @@ -3059,9 +3051,7 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), - ...(providerOptionsForDispatch - ? { providerOptions: providerOptionsForDispatch } - : {}), + ...(providerOptionsForDispatch ? { providerOptions: providerOptionsForDispatch } : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: "default", @@ -3483,490 +3473,504 @@ export default function ChatView({ threadId }: ChatViewProps) {
{/* Chat column */}
- - {/* Messages */} -
- 0} - isWorking={isWorking} - activeTurnInProgress={isWorking || !latestTurnSettled} - activeTurnStartedAt={activeWorkStartedAt} - scrollContainer={messagesScrollElement} - timelineEntries={timelineEntries} - completionDividerBeforeEntryId={completionDividerBeforeEntryId} - completionSummary={completionSummary} - turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId} - nowIso={nowIso} - expandedWorkGroups={expandedWorkGroups} - onToggleWorkGroup={onToggleWorkGroup} - onOpenTurnDiff={onOpenTurnDiff} - revertTurnCountByUserMessageId={revertTurnCountByUserMessageId} - onRevertUserMessage={onRevertUserMessage} - isRevertingCheckpoint={isRevertingCheckpoint} - onImageExpand={onExpandTimelineImage} - markdownCwd={gitCwd ?? undefined} - resolvedTheme={resolvedTheme} - workspaceRoot={activeProject?.cwd ?? undefined} - /> -
- - {/* Input bar */} -
-
+ {/* Messages */}
- {activePendingApproval ? ( -
- -
- ) : pendingUserInputs.length > 0 ? ( -
- -
- ) : showPlanFollowUpPrompt && activeProposedPlan ? ( -
- -
- ) : null} + 0} + isWorking={isWorking} + activeTurnInProgress={isWorking || !latestTurnSettled} + activeTurnStartedAt={activeWorkStartedAt} + scrollContainer={messagesScrollElement} + timelineEntries={timelineEntries} + completionDividerBeforeEntryId={completionDividerBeforeEntryId} + completionSummary={completionSummary} + turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId} + nowIso={nowIso} + expandedWorkGroups={expandedWorkGroups} + onToggleWorkGroup={onToggleWorkGroup} + onOpenTurnDiff={onOpenTurnDiff} + revertTurnCountByUserMessageId={revertTurnCountByUserMessageId} + onRevertUserMessage={onRevertUserMessage} + isRevertingCheckpoint={isRevertingCheckpoint} + onImageExpand={onExpandTimelineImage} + markdownCwd={gitCwd ?? undefined} + resolvedTheme={resolvedTheme} + workspaceRoot={activeProject?.cwd ?? undefined} + /> +
- {/* Textarea area */} -
+ - {composerMenuOpen && !isComposerApprovalState && ( -
- -
- )} +
+ {activePendingApproval ? ( +
+ +
+ ) : pendingUserInputs.length > 0 ? ( +
+ +
+ ) : showPlanFollowUpPrompt && activeProposedPlan ? ( +
+ +
+ ) : null} - {!isComposerApprovalState && pendingUserInputs.length === 0 && composerImages.length > 0 && ( -
- {composerImages.map((image) => ( -
- {image.previewUrl ? ( - - ) : ( -
- {image.name} -
- )} - {nonPersistedComposerImageIdSet.has(image.id) && ( - - - - - } - /> - - Draft attachment could not be saved locally and may be lost on - navigation. - - - )} - + {/* Textarea area */} +
+ {composerMenuOpen && !isComposerApprovalState && ( +
+
- ))} -
- )} - -
+ )} - {/* Bottom toolbar */} - {activePendingApproval ? ( -
- -
- ) : ( -
-
- {/* Provider/model picker */} - 0 && ( +
+ {composerImages.map((image) => ( +
+ {image.previewUrl ? ( + + ) : ( +
+ {image.name} +
+ )} + {nonPersistedComposerImageIdSet.has(image.id) && ( + + + + + } + /> + + Draft attachment could not be saved locally and may be lost on + navigation. + + + )} + +
+ ))} +
+ )} + +
- {selectedProvider === "codex" && selectedEffort != null ? ( - <> - - + +
+ ) : ( +
+
+ {/* Provider/model picker */} + - - ) : null} - {/* Divider */} - + {selectedProvider === "codex" && selectedEffort != null ? ( + <> + + + + ) : null} - {/* Interaction mode toggle */} - - - {/* Divider */} - - - {/* Runtime mode toggle */} - - - {/* Plan sidebar toggle */} - {(activePlan || activeProposedPlan || planSidebarOpen) ? ( - <> + {/* Divider */} + + {/* Interaction mode toggle */} - - ) : null} -
- {/* Right side: send / stop button */} -
- {isPreparingWorktree ? ( - Preparing worktree... - ) : null} - {activePendingProgress ? ( -
- {activePendingProgress.questionIndex > 0 ? ( - - ) : null} + {/* Divider */} + + + {/* Runtime mode toggle */} + + {/* Plan sidebar toggle */} + {activePlan || activeProposedPlan || planSidebarOpen ? ( + <> + + + + ) : null}
- ) : phase === "running" ? ( - - ) : pendingUserInputs.length === 0 ? ( - showPlanFollowUpPrompt ? ( - prompt.trim().length > 0 ? ( - - ) : ( -
+ + {/* Right side: send / stop button */} +
+ {isPreparingWorktree ? ( + + Preparing worktree... + + ) : null} + {activePendingProgress ? ( +
+ {activePendingProgress.questionIndex > 0 ? ( + + ) : null} - - - } - > - - - - void onImplementPlanInNewThread()} - > - Implement in new thread - - -
- ) - ) : ( - + ) : pendingUserInputs.length === 0 ? ( + showPlanFollowUpPrompt ? ( + prompt.trim().length > 0 ? ( + + ) : ( +
+ + + + } + > + + + + void onImplementPlanInNewThread()} + > + Implement in new thread + + + +
+ ) ) : ( - - )} - - ) - ) : null} -
+ {isConnecting || isSendBusy ? ( + + ) : ( + + )} + + ) + ) : null} +
+
+ )}
- )} +
- -
- -
{/* end chat column */} +
+ {/* end chat column */} {/* Plan sidebar */} {planSidebarOpen ? ( @@ -3985,7 +3989,8 @@ export default function ChatView({ threadId }: ChatViewProps) { }} /> ) : null} -
{/* end horizontal flex container */} +
+ {/* end horizontal flex container */} {isGitRepo && ( { if (event.metaKey || event.ctrlKey || event.altKey) return; const target = event.target; - if ( - target instanceof HTMLInputElement || - target instanceof HTMLTextAreaElement - ) { + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { return; } // If the user has started typing a custom answer in the contenteditable @@ -4508,12 +4510,12 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
{option.label} {option.description && option.description !== option.label ? ( - {option.description} + + {option.description} + ) : null}
- {isSelected ? ( - - ) : null} + {isSelected ? : null} ); })} @@ -5575,7 +5577,8 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: { >