From 1735e27d9e5106bbb35d5b1dd10363604a54b69e Mon Sep 17 00:00:00 2001 From: Taras Date: Sat, 18 Jul 2026 22:05:09 +0000 Subject: [PATCH 01/39] Add terminal selection copy action (#2904) --- .../src/components/ThreadTerminalDrawer.tsx | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 1641bb6b109..8591c24c71a 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -31,6 +31,7 @@ import { useState, } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; +import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; import { type TerminalContextSelection } from "~/lib/terminalContext"; import { useOpenInPreferredEditor } from "../editorPreferences"; @@ -330,7 +331,7 @@ export function TerminalViewport({ const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); const selectionGestureActiveRef = useRef(false); const selectionActionRequestIdRef = useRef(0); - const selectionActionOpenRef = useRef(false); + const selectionActionMenuOpenRef = useRef(false); const selectionActionTimerRef = useRef(null); const keybindingsRef = useRef(keybindings); const runtimeEnvKey = useMemo(() => runtimeEnvSignature(runtimeEnv), [runtimeEnv]); @@ -417,6 +418,7 @@ export function TerminalViewport({ const readSelectionAction = (): { position: { x: number; y: number }; + clipboardText: string; selection: TerminalContextSelection; } | null => { const activeTerminal = terminalRef.current; @@ -445,6 +447,7 @@ export function TerminalViewport({ }); return { position, + clipboardText: selectionText, selection: { terminalId, terminalLabel: readTerminalLabel(), @@ -460,7 +463,7 @@ export function TerminalViewport({ clearSelectionAction(); return; } - if (selectionActionOpenRef.current) { + if (selectionActionMenuOpenRef.current) { return; } const nextAction = readSelectionAction(); @@ -469,20 +472,46 @@ export function TerminalViewport({ return; } const requestId = ++selectionActionRequestIdRef.current; - selectionActionOpenRef.current = true; - try { - const clicked = await localApi.contextMenu.show( - [{ id: "add-to-chat", label: "Add to chat" }], + selectionActionMenuOpenRef.current = true; + const clicked = await localApi.contextMenu + .show( + [ + { id: "add-to-chat", label: "Add to chat" }, + { id: "copy", label: "Copy" }, + ], nextAction.position, - ); - if (requestId !== selectionActionRequestIdRef.current || clicked !== "add-to-chat") { + ) + .finally(() => { + selectionActionMenuOpenRef.current = false; + }); + if (requestId !== selectionActionRequestIdRef.current || clicked === null) { + return; + } + switch (clicked) { + case "add-to-chat": + handleAddTerminalContext(nextAction.selection); + terminalRef.current?.clearSelection(); + terminalRef.current?.focus(); + return; + case "copy": + try { + await writeTextToClipboard(nextAction.clipboardText, "terminal selection"); + } catch (error) { + if (requestId !== selectionActionRequestIdRef.current) { + return; + } + const activeTerminal = terminalRef.current; + if (activeTerminal) { + writeSystemMessage( + activeTerminal, + error instanceof Error ? error.message : "Unable to copy terminal selection", + ); + } + } + if (requestId === selectionActionRequestIdRef.current) { + terminalRef.current?.focus(); + } return; - } - handleAddTerminalContext(nextAction.selection); - terminalRef.current?.clearSelection(); - terminalRef.current?.focus(); - } finally { - selectionActionOpenRef.current = false; } }; From 4f11740510d9040475936a24b08f60852bf5fdc4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 19 Jul 2026 11:57:58 +0200 Subject: [PATCH 02/39] Add isolated app testing workflow (#4121) Co-authored-by: codex --- .agents/skills/test-t3-app/SKILL.md | 69 +++++ .agents/skills/test-t3-app/agents/openai.yaml | 4 + .../test-t3-app/references/sqlite-fixtures.md | 58 ++++ AGENTS.md | 33 +- .../src/app/DesktopEnvironment.test.ts | 32 +- apps/desktop/src/app/DesktopEnvironment.ts | 8 +- apps/server/scripts/t3-sqlite-state.test.ts | 119 ++++++++ apps/server/scripts/t3-sqlite-state.ts | 285 ++++++++++++++++++ apps/server/src/cli/config.test.ts | 29 +- apps/server/src/cli/config.ts | 18 +- apps/server/src/config.ts | 10 +- docs/operations/observability.md | 26 +- docs/reference/scripts.md | 8 +- scripts/dev-runner.test.ts | 63 +++- scripts/dev-runner.ts | 36 ++- 15 files changed, 711 insertions(+), 87 deletions(-) create mode 100644 .agents/skills/test-t3-app/SKILL.md create mode 100644 .agents/skills/test-t3-app/agents/openai.yaml create mode 100644 .agents/skills/test-t3-app/references/sqlite-fixtures.md create mode 100644 apps/server/scripts/t3-sqlite-state.test.ts create mode 100644 apps/server/scripts/t3-sqlite-state.ts diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md new file mode 100644 index 00000000000..c573807d615 --- /dev/null +++ b/.agents/skills/test-t3-app/SKILL.md @@ -0,0 +1,69 @@ +--- +name: test-t3-app +description: Launch and test the T3 Code web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, test UI behavior in a browser, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite. +--- + +# Test T3 App + +## Start an isolated web environment + +1. Run commands from the repository root. +2. Choose a base directory that belongs only to the current worktree or test: + - Use the repository's ignored `.t3` directory for reusable worktree-local state. + - Use `mktemp -d /tmp/t3code-test.XXXXXX` for disposable state and retain the printed absolute path. +3. Start the full web stack with `vp run dev --home-dir `. +4. Keep the terminal session alive and read the selected server port, web port, base directory, and pairing URL from its output. + +Treat a base directory as disposable only when it was created or deliberately selected for the current test. Never delete or directly seed the shared `~/.t3` directory. Prefer starting with a new temporary base directory over clearing state of uncertain ownership. + +The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it. + +## Authenticate the browser on the first navigation + +1. Wait for the server log that says authentication is required and includes a URL ending in `/pair#token=...`. +2. Use the controlled in-app browser or browser-automation surface available to the agent. Do not use a system-browser launch command during automated testing. +3. Open that complete URL exactly once as the controlled browser's first navigation. Preserve the fragment and token verbatim. +4. Wait for the pairing exchange and redirect to finish before navigating elsewhere. +5. Continue in the same browser context so its stored bearer session remains available. + +Treat pairing URLs as secrets. Do not copy them into final responses, screenshots, committed files, or durable logs. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it. + +## Recover a consumed or expired pairing token + +Create another token against the same database and web URL as the running dev server: + +```bash +T3CODE_PORT= node apps/server/src/bin.ts auth pairing create \ + --base-dir \ + --dev-url \ + --base-url \ + --ttl 15m \ + --label agent-ui-test +``` + +Use the `Pair URL` from this command once. Derive `` and `` from the current dev-runner output, including any automatically selected port offset. Setting `T3CODE_PORT` keeps the administrative CLI from probing for an unrelated free port. + +Always pass `--dev-url` for a dev-runner environment so the generated pairing URL uses the current web origin. An explicit base directory stores runtime state in `/userdata`; the `/dev` fallback is only used by an implicit dev home. Use `auth pairing list` to inspect active token metadata; it intentionally cannot reveal token secrets. + +## Inspect or seed SQLite state + +Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) before changing the database. + +- Use `node apps/server/scripts/t3-sqlite-state.ts query` for schema discovery and read-only checks. +- Stop the dev server before using `node apps/server/scripts/t3-sqlite-state.ts exec`, then restart it with the same base directory. +- Seed projection tables only for disposable UI fixtures. Use application commands and APIs when testing business behavior or projection correctness. +- Use the auth CLI, not direct `auth_*` table edits, for pairing and sessions. + +The helper refuses to write to the shared `~/.t3` directory by default and creates a database backup before each mutation. + +## Finish the test + +Stop the dev process with its terminal interrupt. Preserve the isolated base directory when it contains useful reproduction evidence; otherwise remove only a path that was created for this test after resolving and verifying the exact target. A fresh isolated base directory is the safest reset when authentication, migrations, or fixture state becomes ambiguous. + +## Troubleshoot predictably + +- If the browser shows an unauthenticated pairing screen, issue a new token instead of retrying the consumed URL. +- If the pairing URL is no longer visible, create a replacement token with both `--dev-url` and `--base-url`. +- If the replacement token is rejected, verify that the CLI and server use the identical absolute base directory and web URL. +- If the UI shows unexpected data, verify that every command uses the identical explicit base directory before editing anything. +- If ports move because another instance is running, trust the current dev-runner output rather than assuming ports `13773` and `5733`. diff --git a/.agents/skills/test-t3-app/agents/openai.yaml b/.agents/skills/test-t3-app/agents/openai.yaml new file mode 100644 index 00000000000..a3b89c95e60 --- /dev/null +++ b/.agents/skills/test-t3-app/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "T3 App Testing" + short_description: "Launch and seed isolated T3 test environments" + default_prompt: "Use $test-t3-app to launch an isolated T3 development environment and test it in the browser." diff --git a/.agents/skills/test-t3-app/references/sqlite-fixtures.md b/.agents/skills/test-t3-app/references/sqlite-fixtures.md new file mode 100644 index 00000000000..8eca543cf97 --- /dev/null +++ b/.agents/skills/test-t3-app/references/sqlite-fixtures.md @@ -0,0 +1,58 @@ +# SQLite fixtures + +Load this reference only when inspecting or seeding local T3 state directly. + +## Select the correct database + +When `--base-dir` or `--home-dir` is explicit, runtime state lives under `/userdata` and the database path is `/userdata/state.sqlite`. The `/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state. + +Start the target runtime once before seeding so all migrations have run. Use an isolated base directory. Stop the server before writes to avoid racing application state or an active projection. + +## Use the helper + +List tables: + +```bash +node apps/server/scripts/t3-sqlite-state.ts query \ + --base-dir \ + --sql "SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name" +``` + +Inspect current columns before writing a fixture: + +```bash +node apps/server/scripts/t3-sqlite-state.ts query \ + --base-dir \ + --sql "PRAGMA table_info(projection_threads)" +``` + +Apply a SQL fixture from a file: + +```bash +node apps/server/scripts/t3-sqlite-state.ts exec \ + --base-dir \ + --file /tmp/t3-seed.sql +``` + +Use one statement per invocation for both `query` and `exec`; the helper wraps writes in a transaction and prints the backup path after a successful mutation. Use a single insert with multiple value rows when a fixture needs several records. + +## Seed projection data carefully + +The web UI primarily reads these projection tables: + +- `projection_projects` +- `projection_threads` +- `projection_thread_messages` +- `projection_thread_activities` +- `projection_thread_sessions` +- `projection_turns` +- `projection_pending_approvals` +- `projection_thread_proposed_plans` + +Inspect `PRAGMA table_info()` and the current migrations under `apps/server/src/persistence/Migrations/` before constructing inserts. Keep identifiers unique, timestamps as ISO strings, JSON columns valid, and related project/thread/turn IDs consistent. + +For a substantial current example, inspect `seedDatabase` in `scripts/mobile-showcase-environment.ts`. Adapt its column set to the target database instead of assuming copied SQL remains current. + +Direct projection writes are appropriate for ephemeral visual states, edge-case counts, long titles, activity lists, and similar UI fixtures. They do not create a coherent orchestration event history. Do not modify `orchestration_events` unless the test specifically exercises projector internals, and do not use direct projection writes to claim backend business behavior works. + +Use the app's commands or APIs for behavior tests. Use `node apps/server/src/bin.ts auth ...` for auth state rather than editing `auth_pairing_links` or `auth_sessions`. diff --git a/AGENTS.md b/AGENTS.md index 380a9202683..0a425cbc185 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,27 +2,14 @@ ## Task Completion Requirements -- `vp check` and `vp run typecheck` must pass before considering tasks completed. - - If changing native mobile code, `vp run lint:mobile` must also pass. -- Use `vp test` for the built-in Vite+ test command and `vp run test` when you specifically need the `test` package script. - -## Project Snapshot - -T3 Code is a minimal web GUI for using coding agents like Codex and Claude. - -This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged. - -## Core Priorities - -1. Performance first. -2. Reliability first. -3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams). - -If a tradeoff is required, choose correctness and robustness over short-term convenience. - -## Maintainability - -Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem. +- Keep local verification focused on the files and packages changed. Run the smallest relevant test set; do not run the full workspace test suite as a routine completion step. + - Use `vp test run ` for focused built-in Vite+ tests. Use `vp run test` only when the affected package specifically requires its `test` script. + - Backend changes must include and run focused tests for the changed behavior. + - Run targeted formatting, lint, and type checks for the affected scope when available. +- Do not run repo-wide `vp check`, `vp run typecheck`, `vp run test`, or equivalent full-suite commands locally unless the user explicitly requests them. CI is responsible for the full verification suite. +- After frontend feature development or any user-visible frontend behavior change, the primary agent must use the `test-t3-app` skill once after integrating the work. Launch one isolated environment, authenticate through the printed pairing URL, and verify the affected flow in the controlled browser. + - Subagents must not independently launch dev servers or repeat integrated browser verification unless their delegated task explicitly requires it. + - Stop dev servers, watchers, and other long-running verification processes when the focused verification is complete. ## Package Roles @@ -35,7 +22,6 @@ Long term maintainability is a core priority. If you add new functionality, firs ## Reference Repos - Open-source Codex repo: https://github.com/openai/codex -- Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor Use these as implementation references when designing protocol handling, UX flows, and operational safeguards. @@ -47,8 +33,7 @@ agents. - Prefer examples and patterns from the vendored source code over generated guesses or web search results. - Do not edit files under `.repos/` unless explicitly asked. - Do not import from `.repos/`; application code must continue importing from normal package dependencies. -- Manage vendored subtrees with `bun run sync:repos`; use `bun run sync:repos --repo ` to sync one - configured repository. +- Manage vendored subtrees with `vpr sync:repos`; use `vpr sync:repos --repo ` to sync one configured repository. - When updating a dependency with a configured vendored subtree, sync that subtree in the same change so `.repos/` matches the installed dependency version. - When writing Effect code, read `.repos/effect-smol/LLMS.md` first and inspect `.repos/effect-smol/` for diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts index 92da3f887ac..15d23f8e152 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts @@ -53,13 +53,16 @@ describe("DesktopEnvironment", () => { assert.equal(environment.isDevelopment, true); assert.equal(environment.appDataDirectory, "/Users/alice/Library/Application Support"); assert.equal(environment.baseDir, "/tmp/t3"); - assert.equal(environment.stateDir, "/tmp/t3/dev"); - assert.equal(environment.desktopSettingsPath, "/tmp/t3/dev/desktop-settings.json"); - assert.equal(environment.clientSettingsPath, "/tmp/t3/dev/client-settings.json"); - assert.equal(environment.savedEnvironmentRegistryPath, "/tmp/t3/dev/saved-environments.json"); - assert.equal(environment.serverSettingsPath, "/tmp/t3/dev/settings.json"); - assert.equal(environment.logDir, "/tmp/t3/dev/logs"); - assert.equal(environment.browserArtifactsDir, "/tmp/t3/dev/browser-artifacts"); + assert.equal(environment.stateDir, "/tmp/t3/userdata"); + assert.equal(environment.desktopSettingsPath, "/tmp/t3/userdata/desktop-settings.json"); + assert.equal(environment.clientSettingsPath, "/tmp/t3/userdata/client-settings.json"); + assert.equal( + environment.savedEnvironmentRegistryPath, + "/tmp/t3/userdata/saved-environments.json", + ); + assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json"); + assert.equal(environment.logDir, "/tmp/t3/userdata/logs"); + assert.equal(environment.browserArtifactsDir, "/tmp/t3/userdata/browser-artifacts"); assert.equal(environment.rootDir, "/repo"); assert.equal(environment.appRoot, "/repo"); assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs"); @@ -78,7 +81,7 @@ describe("DesktopEnvironment", () => { }), ); - it.effect("derives production state paths under userdata", () => + it.effect("stores production state under userdata in an explicit home", () => Effect.gen(function* () { const environment = yield* makeEnvironment( {}, @@ -95,6 +98,19 @@ describe("DesktopEnvironment", () => { }), ); + it.effect("keeps implicit development state separate from production state", () => + Effect.gen(function* () { + const development = yield* makeEnvironment( + {}, + { VITE_DEV_SERVER_URL: "http://localhost:5173" }, + ); + const production = yield* makeEnvironment(); + + assert.equal(development.stateDir, "/Users/alice/.t3/dev"); + assert.equal(production.stateDir, "/Users/alice/.t3/userdata"); + }), + ); + it.effect("uses a configured app user model id override", () => Effect.gen(function* () { const environment = yield* makeEnvironment( diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index 061a9368c53..c991f5b39d6 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -147,7 +147,8 @@ const make = Effect.fn("desktop.environment.make")(function* ( : input.platform === "darwin" ? path.join(homeDirectory, "Library", "Application Support") : Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config")); - const baseDir = Option.getOrElse(config.t3Home, () => path.join(homeDirectory, ".t3")); + const configuredBaseDir = config.t3Home; + const baseDir = Option.getOrElse(configuredBaseDir, () => path.join(homeDirectory, ".t3")); const rootDir = path.resolve(input.dirname, "../../.."); const appRoot = input.isPackaged ? input.appPath : rootDir; const branding = resolveDesktopAppBranding({ @@ -155,7 +156,10 @@ const make = Effect.fn("desktop.environment.make")(function* ( appVersion: input.appVersion, }); const displayName = branding.displayName; - const stateDir = path.join(baseDir, isDevelopment ? "dev" : "userdata"); + const stateDir = path.join( + baseDir, + isDevelopment && Option.isNone(configuredBaseDir) ? "dev" : "userdata", + ); const userDataDirName = isDevelopment ? "t3code-dev" : "t3code"; const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const resourcesPath = input.resourcesPath; diff --git a/apps/server/scripts/t3-sqlite-state.test.ts b/apps/server/scripts/t3-sqlite-state.test.ts new file mode 100644 index 00000000000..d1ef1368918 --- /dev/null +++ b/apps/server/scripts/t3-sqlite-state.test.ts @@ -0,0 +1,119 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; +import { runSqliteState } from "./t3-sqlite-state.ts"; + +const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* ( + baseDir: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const stateDir = path.join(baseDir, "userdata"); + const databasePath = path.join(stateDir, "state.sqlite"); + yield* fs.makeDirectory(stateDir, { recursive: true }); + yield* Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* sql`CREATE TABLE fixtures (id INTEGER PRIMARY KEY, label TEXT NOT NULL)`; + yield* sql`INSERT INTO fixtures (id, label) VALUES (1, 'existing')`; + }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: databasePath }))); +}); + +it.layer(NodeServices.layer)("t3-sqlite-state", (it) => { + it.effect("reports each invalid SQL source with a specific error", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-input-" }); + + const multipleSources = yield* runSqliteState({ + operation: "query", + baseDir, + sql: "SELECT 1", + file: "fixture.sql", + }).pipe(Effect.flip); + assert.equal(multipleSources._tag, "SqliteStateMultipleSqlSourcesError"); + assert.equal(multipleSources.message, "Provide only one of --sql or --file."); + + const missingSource = yield* runSqliteState({ operation: "query", baseDir }).pipe( + Effect.flip, + ); + assert.equal(missingSource._tag, "SqliteStateMissingSqlSourceError"); + assert.equal(missingSource.message, "Provide one of --sql or --file."); + + const emptySql = yield* runSqliteState({ + operation: "query", + baseDir, + sql: " ", + }).pipe(Effect.flip); + assert.equal(emptySql._tag, "SqliteStateEmptySqlError"); + assert.equal(emptySql.message, "SQL input is empty."); + }), + ); + + it.effect("queries an isolated database through Effect SQL", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-query-" }); + yield* createFixtureDatabase(baseDir); + + const result = yield* runSqliteState({ + operation: "query", + baseDir, + sql: "SELECT id, label FROM fixtures", + }); + + assert.equal(result.operation, "query"); + if (result.operation === "query") { + assert.deepStrictEqual(result.rows, [{ id: 1, label: "existing" }]); + } + }), + ); + + it.effect("backs up isolated state before writes and refuses the shared home", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-exec-" }); + yield* createFixtureDatabase(baseDir); + + const mutation = yield* runSqliteState({ + operation: "exec", + baseDir, + sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')", + }); + assert.equal(mutation.operation, "exec"); + if (mutation.operation === "exec") { + assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600); + } + + const error = yield* runSqliteState( + { + operation: "exec", + baseDir, + sql: "DELETE FROM fixtures", + }, + { sharedHome: baseDir }, + ).pipe(Effect.flip); + assert.equal(error._tag, "SqliteStateSharedHomeMutationError"); + + const aliasParent = yield* fs.makeTempDirectoryScoped({ + prefix: "t3-sqlite-state-alias-", + }); + const aliasBaseDir = path.join(aliasParent, "shared-home-alias"); + yield* fs.symlink(baseDir, aliasBaseDir); + const aliasError = yield* runSqliteState( + { + operation: "exec", + baseDir: aliasBaseDir, + sql: "DELETE FROM fixtures", + }, + { sharedHome: baseDir }, + ).pipe(Effect.flip); + assert.equal(aliasError._tag, "SqliteStateSharedHomeMutationError"); + }), + ); +}); diff --git a/apps/server/scripts/t3-sqlite-state.ts b/apps/server/scripts/t3-sqlite-state.ts new file mode 100644 index 00000000000..de0402b3647 --- /dev/null +++ b/apps/server/scripts/t3-sqlite-state.ts @@ -0,0 +1,285 @@ +#!/usr/bin/env node + +// @effect-diagnostics nodeBuiltinImport:off - node:os resolves the shared T3 home guard. +import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as NodeOS from "node:os"; +import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; +import * as Console from "effect/Console"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import { Argument, Command, Flag } from "effect/unstable/cli"; + +import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts"; + +export const SqliteStateOperation = Schema.Literals(["query", "exec"]); +export type SqliteStateOperation = typeof SqliteStateOperation.Type; + +export class SqliteStateMultipleSqlSourcesError extends Schema.TaggedErrorClass()( + "SqliteStateMultipleSqlSourcesError", + {}, +) { + override get message(): string { + return "Provide only one of --sql or --file."; + } +} + +export class SqliteStateMissingSqlSourceError extends Schema.TaggedErrorClass()( + "SqliteStateMissingSqlSourceError", + {}, +) { + override get message(): string { + return "Provide one of --sql or --file."; + } +} + +export class SqliteStateEmptySqlError extends Schema.TaggedErrorClass()( + "SqliteStateEmptySqlError", + {}, +) { + override get message(): string { + return "SQL input is empty."; + } +} + +export class SqliteStateDatabaseMissingError extends Schema.TaggedErrorClass()( + "SqliteStateDatabaseMissingError", + { + databasePath: Schema.String, + }, +) { + override get message(): string { + return `Database does not exist at '${this.databasePath}'. Start T3 once to run migrations.`; + } +} + +export class SqliteStateSharedHomeMutationError extends Schema.TaggedErrorClass()( + "SqliteStateSharedHomeMutationError", + {}, +) { + override get message(): string { + return "Refusing to mutate the shared ~/.t3 database. Use an isolated --base-dir."; + } +} + +export class SqliteStateSqlFileError extends Schema.TaggedErrorClass()( + "SqliteStateSqlFileError", + { + filePath: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to read SQL from '${this.filePath}'.`; + } +} + +export class SqliteStateDatabaseError extends Schema.TaggedErrorClass()( + "SqliteStateDatabaseError", + { + operation: SqliteStateOperation, + databasePath: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to ${this.operation} SQLite database at '${this.databasePath}'.`; + } +} + +const SqliteStateValue = Schema.Union([ + Schema.Null, + Schema.String, + Schema.Number, + Schema.Array(Schema.Number), +]); +const SqliteStateRow = Schema.Record(Schema.String, SqliteStateValue); +const SqliteStateQueryResult = Schema.Struct({ + operation: Schema.Literal("query"), + database: Schema.String, + rows: Schema.Array(SqliteStateRow), +}); +const SqliteStateExecResult = Schema.Struct({ + operation: Schema.Literal("exec"), + database: Schema.String, + backup: Schema.String, +}); +const SqliteStateResult = Schema.Union([SqliteStateQueryResult, SqliteStateExecResult]); +const encodeSqliteStateResult = Schema.encodeEffect(fromJsonStringPretty(SqliteStateResult)); + +export type SqliteStateResult = typeof SqliteStateResult.Type; + +type RawSqliteValue = null | string | number | bigint | Uint8Array; +type RawSqliteRow = Readonly>; + +export interface RunSqliteStateInput { + readonly operation: SqliteStateOperation; + readonly baseDir: string; + readonly sql?: string | undefined; + readonly file?: string | undefined; +} + +export interface RunSqliteStateOptions { + readonly sharedHome?: string | undefined; +} + +const resolveSqlSource = Effect.fn("resolveSqliteStateSqlSource")(function* ( + sql: string | undefined, + file: string | undefined, +) { + if (sql !== undefined && file !== undefined) { + return yield* new SqliteStateMultipleSqlSourcesError(); + } + if (sql === undefined && file === undefined) { + return yield* new SqliteStateMissingSqlSourceError(); + } + + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + let source: string; + if (sql !== undefined) { + source = sql; + } else { + const filePath = path.resolve(file as string); + source = yield* fs + .readFileString(filePath) + .pipe(Effect.mapError((cause) => new SqliteStateSqlFileError({ filePath, cause }))); + } + + const trimmed = source.trim(); + if (trimmed.length === 0) { + return yield* new SqliteStateEmptySqlError(); + } + return trimmed; +}); + +function normalizeSqliteValue(value: RawSqliteValue): typeof SqliteStateValue.Type { + if (typeof value === "bigint") { + const numericValue = Number(value); + return Number.isSafeInteger(numericValue) ? numericValue : value.toString(); + } + if (value instanceof Uint8Array) { + return Array.from(value); + } + return value; +} + +function normalizeSqliteRow(row: RawSqliteRow): typeof SqliteStateRow.Type { + return Object.fromEntries( + Object.entries(row).map(([key, value]) => [key, normalizeSqliteValue(value)]), + ); +} + +export const runSqliteState = Effect.fn("runSqliteState")(function* ( + input: RunSqliteStateInput, + options: RunSqliteStateOptions = {}, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const baseDir = path.resolve(input.baseDir); + const sharedHome = path.resolve(options.sharedHome ?? path.join(NodeOS.homedir(), ".t3")); + const databasePath = path.join(baseDir, "userdata", "state.sqlite"); + const source = yield* resolveSqlSource(input.sql, input.file); + + if (!(yield* fs.exists(databasePath))) { + return yield* new SqliteStateDatabaseMissingError({ databasePath }); + } + if (input.operation === "exec") { + const [canonicalBaseDir, canonicalSharedHome] = yield* Effect.all([ + fs.realPath(baseDir), + fs.realPath(sharedHome).pipe(Effect.orElseSucceed(() => sharedHome)), + ]); + if (canonicalBaseDir === canonicalSharedHome) { + return yield* new SqliteStateSharedHomeMutationError(); + } + } + + const program = Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + yield* sql.unsafe("PRAGMA busy_timeout = 5000").unprepared; + + if (input.operation === "query") { + const rows = yield* sql.unsafe(source).unprepared.pipe( + Effect.provideService(SqlClient.SafeIntegers, true), + Effect.map((rows) => rows.map(normalizeSqliteRow)), + ); + return { + operation: "query", + database: databasePath, + rows, + } as const; + } + + const timestamp = DateTime.formatIso(yield* DateTime.now).replaceAll(":", "-"); + const backupPath = `${databasePath}.backup-${timestamp}`; + yield* sql`VACUUM INTO ${backupPath}`; + yield* fs.chmod(backupPath, 0o600); + yield* sql.withTransaction(sql.unsafe(source).unprepared); + + return { + operation: "exec", + database: databasePath, + backup: backupPath, + } as const; + }); + + return yield* program.pipe( + Effect.provide( + NodeSqliteClient.layer({ + filename: databasePath, + readonly: input.operation === "query", + }), + ), + Effect.mapError( + (cause) => + new SqliteStateDatabaseError({ + operation: input.operation, + databasePath, + cause, + }), + ), + ); +}); + +export const t3SqliteStateCommand = Command.make( + "t3-sqlite-state", + { + operation: Argument.choice("operation", SqliteStateOperation.literals).pipe( + Argument.withDescription("Run a read-only query or a backed-up fixture mutation."), + ), + baseDir: Flag.string("base-dir").pipe( + Flag.withDescription("Explicit T3 base directory containing userdata/state.sqlite."), + ), + sql: Flag.string("sql").pipe( + Flag.optional, + Flag.withDescription("SQL source supplied directly on the command line."), + ), + file: Flag.string("file").pipe( + Flag.optional, + Flag.withDescription("Path to a SQL source file."), + ), + }, + ({ operation, baseDir, sql, file }) => + runSqliteState({ + operation, + baseDir, + sql: Option.getOrUndefined(sql), + file: Option.getOrUndefined(file), + }).pipe(Effect.flatMap(encodeSqliteStateResult), Effect.flatMap(Console.log)), +).pipe( + Command.withDescription( + "Inspect or seed an isolated T3 SQLite database with automatic backups for writes.", + ), +); + +if (import.meta.main) { + Command.run(t3SqliteStateCommand, { version: "0.0.0" }).pipe( + Effect.provide(NodeServices.layer), + NodeRuntime.runMain, + ); +} diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index d4d9d378557..f6c2a63e192 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -18,6 +18,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { deriveServerPaths } from "../config.ts"; import { resolveServerConfig } from "./config.ts"; +const deriveExplicitServerPaths = (baseDir: string, devUrl: URL | undefined) => + deriveServerPaths(baseDir, devUrl, { baseDirIsExplicit: true }); + const encodeDesktopBootstrap = Schema.encodeEffect(Schema.fromJsonString(DesktopBackendBootstrap)); const makeDesktopBootstrap = ( @@ -60,7 +63,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { Effect.gen(function* () { const { join } = yield* Path.Path; const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-env-base"); - const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:5173")); + const derivedPaths = yield* deriveExplicitServerPaths( + baseDir, + new URL("http://127.0.0.1:5173"), + ); const resolved = yield* resolveServerConfig( { mode: Option.none(), @@ -119,6 +125,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { tailscaleServeEnabled: false, tailscaleServePort: 443, }); + assert.equal(resolved.stateDir, join(baseDir, "userdata")); }), ); @@ -126,7 +133,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { Effect.gen(function* () { const { join } = yield* Path.Path; const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-flags-base"); - const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173")); + const derivedPaths = yield* deriveExplicitServerPaths( + baseDir, + new URL("http://127.0.0.1:4173"), + ); const resolved = yield* resolveServerConfig( { mode: Option.some("web"), @@ -185,6 +195,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { tailscaleServeEnabled: true, tailscaleServePort: 8443, }); + assert.equal(resolved.dbPath, join(baseDir, "userdata", "state.sqlite")); }), ); @@ -199,7 +210,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { tailscaleServePort: 443, }), ); - const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173")); + const derivedPaths = yield* deriveExplicitServerPaths( + baseDir, + new URL("http://127.0.0.1:4173"), + ); const resolved = yield* resolveServerConfig( { @@ -396,7 +410,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { tailscaleServePort: 443, }), ); - const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173")); + const derivedPaths = yield* deriveExplicitServerPaths( + baseDir, + new URL("http://127.0.0.1:4173"), + ); const resolved = yield* resolveServerConfig( { @@ -461,7 +478,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-settings-" }); - const derivedPaths = yield* deriveServerPaths(baseDir, undefined); + const derivedPaths = yield* deriveExplicitServerPaths(baseDir, undefined); yield* fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }); yield* fs.writeFileString( derivedPaths.settingsPath, @@ -529,7 +546,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { Effect.gen(function* () { const { join } = yield* Path.Path; const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-headless-base"); - const derivedPaths = yield* deriveServerPaths(baseDir, undefined); + const derivedPaths = yield* deriveExplicitServerPaths(baseDir, undefined); const resolved = yield* resolveServerConfig( { diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 7a9cd72d526..5a4cde0a6fd 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -31,7 +31,9 @@ export const hostFlag = Flag.string("host").pipe( Flag.optional, ); export const baseDirFlag = Flag.string("base-dir").pipe( - Flag.withDescription("Base directory path (equivalent to T3CODE_HOME)."), + Flag.withDescription( + "Explicit T3 Code data directory; runtime state is stored under userdata (equivalent to T3CODE_HOME).", + ), Flag.optional, ); export const devUrlFlag = Flag.string("dev-url").pipe( @@ -259,19 +261,21 @@ export const resolveServerConfig = ( resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)), () => undefined, ); + const explicitBaseDir = resolveOptionPrecedence( + normalizedFlags.baseDir, + Option.fromUndefinedOr(env.t3Home), + ).pipe(Option.filter((value) => value.trim().length > 0)); const baseDir = yield* resolveBaseDir( Option.getOrUndefined( - resolveOptionPrecedence( - normalizedFlags.baseDir, - Option.fromUndefinedOr(env.t3Home), - Option.fromUndefinedOr(bootstrap?.t3Home), - ), + resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)), ), ); const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd()); const cwd = path.resolve(yield* expandHomePath(rawCwd.trim())); yield* fs.makeDirectory(cwd, { recursive: true }); - const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl); + const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl, { + baseDirIsExplicit: Option.isSome(explicitBaseDir), + }); yield* ServerConfig.ensureServerDirectories(derivedPaths); const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings( derivedPaths.settingsPath, diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 2608ccc16ae..3b081c95c34 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -45,6 +45,10 @@ export interface ServerDerivedPaths { readonly secretsDir: string; } +export interface DeriveServerPathsOptions { + readonly baseDirIsExplicit?: boolean; +} + /** * ServerConfig - Service tag for server runtime configuration. */ @@ -91,9 +95,13 @@ export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerCo export const deriveServerPaths = Effect.fn(function* ( baseDir: ServerConfig["Service"]["baseDir"], devUrl: ServerConfig["Service"]["devUrl"], + options: DeriveServerPathsOptions = {}, ): Effect.fn.Return { const { join } = yield* Path.Path; - const stateDir = join(baseDir, devUrl !== undefined ? "dev" : "userdata"); + const stateDir = join( + baseDir, + devUrl !== undefined && !options.baseDirIsExplicit ? "dev" : "userdata", + ); const dbPath = join(stateDir, "state.sqlite"); const attachmentsDir = join(stateDir, "attachments"); const logsDir = join(stateDir, "logs"); diff --git a/docs/operations/observability.md b/docs/operations/observability.md index 5b98d1163ea..1d55894b182 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -165,16 +165,24 @@ The backend reads observability config at process start. If you change OTLP env The trace file is the fastest way to inspect raw span data. +Resolve the production or explicitly configured trace file once. Runtime state lives under the +base directory's `userdata` folder: + +```bash +TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson" +``` + Tail it: ```bash -tail -f "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +tail -f "$TRACE_FILE" ``` -In monorepo dev, use: +For an implicit monorepo dev server, use: ```bash -tail -f ./dev/logs/server.trace.ndjson +TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson" +tail -f "$TRACE_FILE" ``` Show failed spans: @@ -185,7 +193,7 @@ jq -c 'select(.exit._tag != "Success") | { durationMs, exit, attributes -}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +}' "$TRACE_FILE" ``` Show slow spans: @@ -196,7 +204,7 @@ jq -c 'select(.durationMs > 1000) | { durationMs, traceId, spanId -}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +}' "$TRACE_FILE" ``` Inspect embedded log events: @@ -213,7 +221,7 @@ jq -c 'select(any(.events[]?; .attributes["effect.logLevel"] != null)) | { level: .attributes["effect.logLevel"] } ] -}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +}' "$TRACE_FILE" ``` Follow one trace: @@ -224,7 +232,7 @@ jq -r 'select(.traceId == "TRACE_ID_HERE") | [ .spanId, (.parentSpanId // "-"), .durationMs -] | @tsv' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +] | @tsv' "$TRACE_FILE" ``` Filter orchestration commands: @@ -235,7 +243,7 @@ jq -c 'select(.attributes["orchestration.command_type"] != null) | { durationMs, commandType: .attributes["orchestration.command_type"], aggregateKind: .attributes["orchestration.aggregate_kind"] -}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +}' "$TRACE_FILE" ``` Filter git activity: @@ -250,7 +258,7 @@ jq -c 'select(.attributes["git.operation"] != null) | { .events[] | select(.name == "git.hook.started" or .name == "git.hook.finished") ] -}' "$T3CODE_HOME/userdata/logs/server.trace.ndjson" +}' "$TRACE_FILE" ``` ### Use Tempo When You Need A Real Trace Viewer diff --git a/docs/reference/scripts.md b/docs/reference/scripts.md index 6bdea266665..746aa66d563 100644 --- a/docs/reference/scripts.md +++ b/docs/reference/scripts.md @@ -3,13 +3,15 @@ - `vp run dev` — Starts contracts, server, and web in watch mode. - `vp run dev:server` — Starts just the WebSocket server. The server process runs on Bun (`@effect/platform-bun` + `BunPtyAdapter`), but task running uses `vp run`. - `vp run dev:web` — Starts just the Vite dev server for the web app. -- Dev commands default `T3CODE_HOME` to `~/.t3` — the same shared home the desktop/production app uses. Override with `--home-dir` (see below) to keep dev state separate. -- Override server CLI-equivalent flags from root dev commands with `--`, for example: - `vp run dev -- --home-dir ~/.t3-2` +- Dev commands implicitly use `~/.t3/dev`, keeping development state separate from `~/.t3/userdata`. An explicit `--home-dir ` stores state under `/userdata`; the base directory remains available for caches, worktrees, and other shared data. +- Web dev commands do not auto-open a browser. Open the one-time pairing URL printed by the server so the first browser navigation is authenticated. Set `T3CODE_NO_BROWSER=0` only when interactive auto-open is intentional. +- Pass dev-runner flags directly after the root task name, for example: + `vp run dev --home-dir /tmp/t3code-dev` - `vp run start` — Runs the production server (serves built web app as static files). - `vp run build` — Builds contracts, web app, and server. - `vp run typecheck` — Strict TypeScript checks for all packages. - `vp run test` — Runs workspace tests. +- `node apps/server/scripts/t3-sqlite-state.ts --base-dir ...` — Inspects or seeds an isolated T3 SQLite database; writes create a private backup first. - `vp run dist:desktop:artifact -- --platform --target --arch ` — Builds a desktop artifact for a specific platform/target/arch. - `vp run dist:desktop:dmg` — Builds a shareable macOS `.dmg` into `./release`. - `vp run dist:desktop:dmg:x64` — Builds an Intel macOS `.dmg`. diff --git a/scripts/dev-runner.test.ts b/scripts/dev-runner.test.ts index 432b50cf728..3b79db49f5b 100644 --- a/scripts/dev-runner.test.ts +++ b/scripts/dev-runner.test.ts @@ -1,5 +1,4 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; -import * as NodeOS from "node:os"; import * as NetService from "@t3tools/shared/Net"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { assert, describe, it } from "@effect/vitest"; @@ -52,7 +51,7 @@ function mockProcess(exit: number | PlatformError.PlatformError) { const devServerInput = { mode: "dev:server", t3Home: "/tmp/t3code-dev-runner", - noBrowser: undefined, + browser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, @@ -127,16 +126,56 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { }); describe("createDevRunnerEnv", () => { - it.effect("defaults T3CODE_HOME to ~/.t3 when not provided", () => + it.effect("leaves the shared home implicit and disables browser auto-open", () => + Effect.gen(function* () { + const env = yield* createDevRunnerEnv({ + mode: "dev", + baseEnv: {}, + serverOffset: 0, + webOffset: 0, + t3Home: undefined, + browser: undefined, + autoBootstrapProjectFromCwd: undefined, + logWebSocketEvents: undefined, + host: undefined, + port: undefined, + devUrl: undefined, + }); + + assert.equal(env.T3CODE_HOME, undefined); + assert.equal(env.T3CODE_NO_BROWSER, "1"); + }), + ); + + it.effect("allows browser auto-open to be explicitly enabled", () => Effect.gen(function* () { - const path = yield* Path.Path; const env = yield* createDevRunnerEnv({ mode: "dev", baseEnv: {}, serverOffset: 0, webOffset: 0, t3Home: undefined, - noBrowser: undefined, + browser: true, + autoBootstrapProjectFromCwd: undefined, + logWebSocketEvents: undefined, + host: undefined, + port: undefined, + devUrl: undefined, + }); + + assert.equal(env.T3CODE_NO_BROWSER, "0"); + }), + ); + + it.effect("requires the browser flag even when the environment enables auto-open", () => + Effect.gen(function* () { + const env = yield* createDevRunnerEnv({ + mode: "dev", + baseEnv: { T3CODE_NO_BROWSER: "0" }, + serverOffset: 0, + webOffset: 0, + t3Home: undefined, + browser: false, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, @@ -144,7 +183,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { devUrl: undefined, }); - assert.equal(env.T3CODE_HOME, path.resolve(NodeOS.homedir(), ".t3")); + assert.equal(env.T3CODE_NO_BROWSER, "1"); }), ); @@ -157,7 +196,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: "/tmp/custom-t3", - noBrowser: true, + browser: false, autoBootstrapProjectFromCwd: false, logWebSocketEvents: true, host: "0.0.0.0", @@ -187,7 +226,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: undefined, - noBrowser: undefined, + browser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, @@ -210,7 +249,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: undefined, - noBrowser: undefined, + browser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: false, host: undefined, @@ -231,7 +270,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: "/tmp/my-t3", - noBrowser: undefined, + browser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, @@ -259,7 +298,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: "/tmp/my-t3", - noBrowser: true, + browser: true, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: "127.0.0.1", @@ -288,7 +327,7 @@ it.layer(NodeServices.layer)("dev-runner", (it) => { serverOffset: 0, webOffset: 0, t3Home: undefined, - noBrowser: undefined, + browser: undefined, autoBootstrapProjectFromCwd: undefined, logWebSocketEvents: undefined, host: undefined, diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index 2953a2f4dec..1938232300f 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -221,7 +221,7 @@ interface CreateDevRunnerEnvInput { readonly serverOffset: number; readonly webOffset: number; readonly t3Home: string | undefined; - readonly noBrowser: boolean | undefined; + readonly browser: boolean | undefined; readonly autoBootstrapProjectFromCwd: boolean | undefined; readonly logWebSocketEvents: boolean | undefined; readonly host: string | undefined; @@ -235,7 +235,7 @@ export function createDevRunnerEnv({ serverOffset, webOffset, t3Home, - noBrowser, + browser, autoBootstrapProjectFromCwd, logWebSocketEvents, host, @@ -245,7 +245,8 @@ export function createDevRunnerEnv({ return Effect.gen(function* () { const serverPort = port ?? BASE_SERVER_PORT + serverOffset; const webPort = BASE_WEB_PORT + webOffset; - const resolvedBaseDir = yield* resolveBaseDir(t3Home); + const configuredBaseDir = t3Home?.trim() || baseEnv.T3CODE_HOME?.trim() || undefined; + const resolvedBaseDir = yield* resolveBaseDir(configuredBaseDir); const isDesktopMode = mode === "dev:desktop"; const output: NodeJS.ProcessEnv = { @@ -254,9 +255,14 @@ export function createDevRunnerEnv({ VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://${isDesktopMode ? DESKTOP_DEV_LOOPBACK_HOST : "localhost"}:${webPort}`, - T3CODE_HOME: resolvedBaseDir, }; + if (configuredBaseDir !== undefined) { + output.T3CODE_HOME = resolvedBaseDir; + } else { + delete output.T3CODE_HOME; + } + if (!isDesktopMode) { output.T3CODE_PORT = String(serverPort); output.VITE_HTTP_URL = `http://localhost:${serverPort}`; @@ -274,10 +280,8 @@ export function createDevRunnerEnv({ output.T3CODE_HOST = host; } - if (!isDesktopMode && noBrowser !== undefined) { - output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0"; - } else if (!isDesktopMode) { - delete output.T3CODE_NO_BROWSER; + if (!isDesktopMode) { + output.T3CODE_NO_BROWSER = browser === true ? "0" : "1"; } if (autoBootstrapProjectFromCwd !== undefined) { @@ -469,7 +473,7 @@ export function resolveModePortOffsets({ interface DevRunnerCliInput { readonly mode: DevMode; readonly t3Home: string | undefined; - readonly noBrowser: boolean | undefined; + readonly browser: boolean | undefined; readonly autoBootstrapProjectFromCwd: boolean | undefined; readonly logWebSocketEvents: boolean | undefined; readonly host: string | undefined; @@ -507,7 +511,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { serverOffset, webOffset, t3Home: input.t3Home, - noBrowser: input.noBrowser, + browser: input.browser, autoBootstrapProjectFromCwd: input.autoBootstrapProjectFromCwd, logWebSocketEvents: input.logWebSocketEvents, host: input.host, @@ -519,9 +523,10 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { serverOffset !== offset || webOffset !== offset ? ` selectedOffset(server=${serverOffset},web=${webOffset})` : ""; + const baseDir = env.T3CODE_HOME ?? (yield* DEFAULT_T3_HOME); yield* Effect.logInfo( - `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.T3CODE_PORT)} webPort=${String(env.PORT)} baseDir=${String(env.T3CODE_HOME)}`, + `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.T3CODE_PORT)} webPort=${String(env.PORT)} baseDir=${baseDir}`, ); if (input.dryRun) { @@ -586,12 +591,13 @@ const devRunnerCli = Command.make("dev-runner", { Argument.withDescription("Development mode to run."), ), t3Home: Flag.string("home-dir").pipe( - Flag.withDescription("Base directory for all T3 Code data (equivalent to T3CODE_HOME)."), + Flag.withDescription( + "Explicit T3 Code data directory; runtime state is stored under userdata (equivalent to T3CODE_HOME).", + ), Flag.withFallbackConfig(optionalStringConfig("T3CODE_HOME")), ), - noBrowser: Flag.boolean("no-browser").pipe( - Flag.withDescription("Browser auto-open toggle (equivalent to T3CODE_NO_BROWSER)."), - Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_NO_BROWSER")), + browser: Flag.boolean("browser").pipe( + Flag.withDescription("Open a browser automatically (disabled by default for web dev)."), ), autoBootstrapProjectFromCwd: Flag.boolean("auto-bootstrap-project-from-cwd").pipe( Flag.withDescription( From 53e3c98a55237ab7327dc1fb55f1f7cf500560a1 Mon Sep 17 00:00:00 2001 From: maria Date: Sun, 19 Jul 2026 06:35:58 -0400 Subject: [PATCH 03/39] feat(web): themed sidebar header art for nightly and dev builds (#4130) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Julius Marminge Co-authored-by: Julius Marminge --- apps/web/src/components/AppSidebarLayout.tsx | 24 +- apps/web/src/components/Sidebar.tsx | 57 +++- .../src/components/SidebarStageBackdrop.tsx | 316 ++++++++++++++++++ apps/web/src/index.css | 36 ++ 4 files changed, 413 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/SidebarStageBackdrop.tsx diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 07ec0dc9077..587ec65cbc9 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -4,10 +4,18 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; import { isElectron } from "../env"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; -import { isMacPlatform } from "../lib/utils"; +import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; import ThreadSidebar from "./Sidebar"; -import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "./ui/sidebar"; +import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; +import { + Sidebar, + SidebarProvider, + SidebarRail, + SidebarTrigger, + useSidebar, + useSidebarVisibility, +} from "./ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; @@ -18,6 +26,8 @@ const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); + const isSidebarVisible = useSidebarVisibility(); + const stageBackdropVariant = useSidebarStageBackdropVariant(); const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle"); useEffect(() => { @@ -42,7 +52,15 @@ function SidebarControl() { + } /> diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 6a76494daa3..38e9f7a828a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -77,7 +77,7 @@ import { isElectron } from "../env"; import { APP_STAGE_LABEL } from "../branding"; import { useOpenPrLink } from "../lib/openPullRequestLink"; import { isTerminalFocused } from "../lib/terminalFocus"; -import { isMacPlatform } from "../lib/utils"; +import { cn, isMacPlatform } from "../lib/utils"; import { readThreadShell, useProject, @@ -126,6 +126,7 @@ import { import { stackedThreadToast, toastManager } from "./ui/toast"; import { formatRelativeTimeLabel } from "../timestampFormat"; import { SettingsSidebarNav } from "./settings/SettingsSidebarNav"; +import { SidebarStageBackdrop, resolveSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; import { Kbd } from "./ui/kbd"; import { getArm64IntelBuildWarningDescription, @@ -2744,33 +2745,55 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ }: { isElectron: boolean; }) { - return isElectron ? ( - - - - - ) : ( - - - + const stageLabel = useSidebarStageLabel(); + const backdropVariant = resolveSidebarStageBackdropVariant(stageLabel); + + return ( + + {backdropVariant ? : null} + + ); }); -function SidebarBrand() { - const stageLabel = useSidebarStageLabel(); - +function SidebarBrand({ stageLabel, onBackdrop }: { stageLabel: string; onBackdrop: boolean }) { return ( - + Code - + {stageLabel} @@ -2791,7 +2814,7 @@ function T3Wordmark() { return ( diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx new file mode 100644 index 00000000000..963a8c2a951 --- /dev/null +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -0,0 +1,316 @@ +import { useAtomValue } from "@effect/atom-react"; + +import { APP_STAGE_LABEL } from "../branding"; +import { resolveServerBackedAppStageLabel } from "../branding.logic"; +import { primaryServerConfigAtom } from "../state/server"; + +export type SidebarStageBackdropVariant = "nightly" | "dev"; + +// A wide viewBox keeps the 96-unit art height at a fixed scale while sidebar resizing reveals +// more horizontal canvas instead of zooming the scene. +const STAGE_BACKDROP_VIEW_BOX = "0 0 8192 96"; + +export function resolveSidebarStageBackdropVariant( + stageLabel: string, +): SidebarStageBackdropVariant | null { + const normalized = stageLabel.trim().toLowerCase(); + if (normalized === "nightly") return "nightly"; + if (normalized === "dev") return "dev"; + return null; +} + +export function useSidebarStageBackdropVariant(): SidebarStageBackdropVariant | null { + const primaryServerVersion = + useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null; + + return resolveSidebarStageBackdropVariant( + resolveServerBackedAppStageLabel({ + primaryServerVersion, + fallbackStageLabel: APP_STAGE_LABEL, + }), + ); +} + +/** Stage-channel header art; palettes mirror the per-channel app icons in `assets/`. */ +export function SidebarStageBackdrop({ variant }: { variant: SidebarStageBackdropVariant }) { + return ( +
+ {variant === "nightly" ? : } +
+ ); +} + +const NIGHTLY_STARS: ReadonlyArray<{ + cx: number; + cy: number; + r: number; + opacity: number; +}> = [ + { cx: 14, cy: 10, r: 0.6, opacity: 0.85 }, + { cx: 38, cy: 22, r: 0.4, opacity: 0.55 }, + { cx: 58, cy: 8, r: 0.5, opacity: 0.7 }, + { cx: 84, cy: 16, r: 0.4, opacity: 0.5 }, + { cx: 104, cy: 7, r: 0.6, opacity: 0.8 }, + { cx: 126, cy: 20, r: 0.4, opacity: 0.55 }, + { cx: 148, cy: 11, r: 0.5, opacity: 0.7 }, + { cx: 170, cy: 24, r: 0.4, opacity: 0.5 }, + { cx: 192, cy: 9, r: 0.6, opacity: 0.8 }, + { cx: 214, cy: 18, r: 0.4, opacity: 0.55 }, + { cx: 236, cy: 8, r: 0.5, opacity: 0.7 }, + { cx: 258, cy: 20, r: 0.45, opacity: 0.6 }, + { cx: 278, cy: 11, r: 0.55, opacity: 0.75 }, + { cx: 26, cy: 34, r: 0.4, opacity: 0.45 }, + { cx: 118, cy: 34, r: 0.4, opacity: 0.45 }, + { cx: 202, cy: 32, r: 0.4, opacity: 0.5 }, + { cx: 268, cy: 34, r: 0.4, opacity: 0.45 }, +]; + +const NIGHTLY_SPARKLES: ReadonlyArray<{ x: number; y: number }> = [ + { x: 70, y: 28 }, + { x: 160, y: 36 }, + { x: 246, y: 26 }, +]; + +function NightlySkyArt() { + return ( + + + + + + + + + + + + + + + + + + + + + + + {NIGHTLY_STARS.map((star) => ( + + ))} + + + {NIGHTLY_SPARKLES.map((sparkle) => ( + + + + + ))} + + + + + + + + + + + + + + + + + + + ); +} + +function DevBlueprintArt() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index c500eb68d7f..77a5f027c13 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -231,6 +231,42 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } + /* Stage-channel sidebar art; ::after ramps to the sidebar bg color and the + mask lets the surface grain show through at the boundary. */ + .sidebar-stage-backdrop { + mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 92%); + } + + .sidebar-stage-backdrop::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + transparent 0%, + transparent 28%, + color-mix(in srgb, var(--app-chrome-background) 10%, transparent) 40%, + color-mix(in srgb, var(--app-chrome-background) 30%, transparent) 52%, + color-mix(in srgb, var(--app-chrome-background) 58%, transparent) 64%, + color-mix(in srgb, var(--app-chrome-background) 82%, transparent) 75%, + color-mix(in srgb, var(--app-chrome-background) 96%, transparent) 85%, + var(--app-chrome-background) 93% + ); + } + + .stage-blueprint { + --stage-bp-top: #67c2ff; + --stage-bp-mid: #347ff8; + --stage-bp-bottom: #1538d0; + } + + .dark .stage-blueprint { + --stage-bp-top: #3a7ad1; + --stage-bp-mid: #2050ae; + --stage-bp-bottom: #101f6e; + } + .workspace-topbar { display: flex; height: var(--workspace-topbar-height); From 7a820abfddbf77166d16614bb9bcabe8fcd2c829 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sun, 19 Jul 2026 09:59:23 -0700 Subject: [PATCH 04/39] feat: add headless `t3 connect` setup for SSH hosts (#3749) Co-authored-by: Claude Fable 5 Co-authored-by: Julius Marminge Co-authored-by: codex --- .env.example | 4 + .plans/t3-connect-remote-setup.html | 257 +++++++++ apps/server/src/bin.test.ts | 6 +- apps/server/src/cli/connect.test.ts | 42 ++ apps/server/src/cli/connect.ts | 301 ++++++++-- apps/server/src/cloud/CliTokenManager.test.ts | 264 +++++++++ apps/server/src/cloud/CliTokenManager.ts | 351 +++++++++--- apps/server/src/cloud/bootService.test.ts | 529 ++++++++++++++++++ apps/server/src/cloud/bootService.ts | 452 +++++++++++++++ apps/server/src/cloud/cliAuthHtml.test.ts | 13 + apps/server/src/cloud/cliAuthHtml.ts | 140 +++++ apps/server/src/cloud/http.test.ts | 1 + apps/server/src/cloud/publicConfig.test.ts | 35 ++ apps/server/src/cloud/publicConfig.ts | 41 +- apps/server/src/persistence/Migrations.ts | 12 +- apps/server/src/server.ts | 5 +- apps/web/src/cloud/connectCliAuth.test.ts | 69 +++ apps/web/src/cloud/connectCliAuth.ts | 91 +++ apps/web/src/cloud/publicConfig.ts | 2 +- .../src/components/SidebarStageBackdrop.tsx | 6 +- .../src/components/auth/AuthSurfaceShell.tsx | 44 ++ .../cloud/ConnectCliAuthSurface.tsx | 188 +++++++ apps/web/src/hostedPairing.ts | 4 +- apps/web/src/routeTree.gen.ts | 42 ++ apps/web/src/routes/__root.tsx | 2 +- apps/web/src/routes/connect.tsx | 13 + apps/web/src/routes/connect_.callback.tsx | 13 + apps/web/src/vite-env.d.ts | 1 + apps/web/vite.config.ts | 4 + packages/shared/package.json | 4 + packages/shared/src/connectAuth.test.ts | 78 +++ packages/shared/src/connectAuth.ts | 124 ++++ packages/shared/src/hostProcess.ts | 14 + packages/shared/src/remote.ts | 2 +- scripts/lib/public-config.ts | 7 +- 35 files changed, 3017 insertions(+), 144 deletions(-) create mode 100644 .plans/t3-connect-remote-setup.html create mode 100644 apps/server/src/cloud/CliTokenManager.test.ts create mode 100644 apps/server/src/cloud/bootService.test.ts create mode 100644 apps/server/src/cloud/bootService.ts create mode 100644 apps/server/src/cloud/cliAuthHtml.test.ts create mode 100644 apps/server/src/cloud/cliAuthHtml.ts create mode 100644 apps/web/src/cloud/connectCliAuth.test.ts create mode 100644 apps/web/src/cloud/connectCliAuth.ts create mode 100644 apps/web/src/components/auth/AuthSurfaceShell.tsx create mode 100644 apps/web/src/components/cloud/ConnectCliAuthSurface.tsx create mode 100644 apps/web/src/routes/connect.tsx create mode 100644 apps/web/src/routes/connect_.callback.tsx create mode 100644 packages/shared/src/connectAuth.test.ts create mode 100644 packages/shared/src/connectAuth.ts diff --git a/.env.example b/.env.example index 79b2adaf0c8..61cdd66d246 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,10 @@ # Get this from your relay deployment. `infra/relay` deploys update it automatically. # T3CODE_RELAY_URL=https://relay.example.com +# Optional: hosted app origin used by the CLI's out-of-band OAuth flow. +# Defaults to https://app.t3.codes; override to test against a staging deployment. +# T3CODE_HOSTED_APP_URL=https://nightly.app.t3.codes + # Public, ingest-only mobile OpenTelemetry configuration. # T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces # T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev diff --git a/.plans/t3-connect-remote-setup.html b/.plans/t3-connect-remote-setup.html new file mode 100644 index 00000000000..101c293bee1 --- /dev/null +++ b/.plans/t3-connect-remote-setup.html @@ -0,0 +1,257 @@ + + + + + +Plan: seamless `npx t3 connect` for remote boxes + + + +
+ +

Seamless npx t3 connect for remote boxes

+

Design principle: the smallest diff that ships the UX. No relay/infra changes, no new backend surface, no new auth primitives — every step reuses code that already exists. One PR, built as four phases with clear commit boundaries — each phase compiles, passes tests, and leaves the product working, so the PR reviews commit-by-commit. (Phase 4, web-triggered update, is an optional follow-up PR.)

+ +
+$ npx t3 connect

+To set up T3 Connect, open this URL and sign in:
+  https://app.t3.codes/connect#B64URL_STATE_AND_CHALLENGE

+Enter your authentication code: [code]

+Connected as theo@t3.gg!

+Run T3 Code in the background whenever this machine boots? (y/n): y

+T3 Code is set up and ready to go. +
+ +

Why this is a small change

+

The entire t3 connect data plane already works: Clerk PKCE token exchange, encrypted secret store, cloudflared relay-client install, relay environment linking, DPoP tokens. The only broken piece on an SSH box is the redirect: CliTokenManager.login() hardcodes a loopback callback (http://127.0.0.1:34338/callback) that requires a browser on the same machine.

+

We swap that one leg for a hosted out-of-band authorization page and keep everything else. Because PKCE's code_verifier never leaves the box, the displayed one-time code is useless to anyone who sees it — no new token-minting or storage is needed anywhere.

+ +
+

Reused as-is (zero changes)

+
    +
  • exchangeToken() PKCE exchange — apps/server/src/cloud/CliTokenManager.ts:147
  • +
  • Token persistence in ServerSecretStore (cloud-cli-oauth-token)
  • +
  • acquireRelayClientForLink() cloudflared install + progress — cli/connect.ts:146
  • +
  • CliState.setCliDesiredCloudLink() + server-side provisioning on start
  • +
  • All relay endpoints (infra/relay) and contracts — untouched
  • +
  • Existing subcommands login/link/status/unlink/logout — semantics unchanged
  • +
  • Web app Clerk session + hosted-page precedent (routes/pair.tsx, hostedPairing.ts)
  • +
+
+ +

Auth flow (hosted out-of-band OAuth, Clerk PKCE)

+ +
+ + + + + + Remote box — t3 CLI + Laptop — app.t3.codes + Clerk + + + + + + + 1. gen verifier + challenge + state + + + + + 2. user opens /connect#{state,challenge} + + + + + 3. sign in → /oauth/authorize (PKCE) + + + + + 4. redirect /connect/callback?code&state + + + + 5. shows account + authorization code + + + + + 6. user enters code in terminal + + + + + 7. POST /oauth/token {code + verifier} → access/refresh tokens + + + + 8. store token, set desired link, + install relay client → Connected! + +
The verifier never leaves the box (steps 1→7), so the authorization code is worthless if observed. state/challenge ride the URL fragment — they are not secrets.
+
+ +
+

Details that keep it simple

+
    +
  • Stateless URL, no short-link service. The /connect page reads state + code_challenge from the URL fragment and builds the Clerk authorize URL client-side. ~100-char URL — fine to transfer into an SSH session.
  • +
  • State check without a backend: the callback page displays one authorization blob of code.state; the CLI splits it and verifies state matches what it generated. One line on each side, preserves the loopback flow's CSRF check.
  • +
  • Phishing is addressed with copy, not code: the callback page shows which account is being connected ("Connecting as theo@…") and warns: "Only enter this code in a terminal session you started yourself." No mechanism needed.
  • +
  • Code expiry is a non-issue: Clerk auth codes live 10 minutes — the same timeout the existing loopback flow already uses. Wrong/expired code → friendly retry that reprints the URL.
  • +
  • One external config step: register https://app.t3.codes/connect/callback as an allowed redirect URI on the existing Clerk CLI OAuth client. No new client, no new scopes.
  • +
+
+ +

The phases (one PR, one commit each)

+

Ordering is dependency order: each phase is independently revertable and the tree is green at every boundary. Phases 1–3 are the PR; phase 4 ships separately later.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PhaseScopeFiles~LOC
1Hosted code page (web-only, purely additive, zero risk). Two static routes modeled on pair.tsx: /connect (ensure Clerk session, then client-side redirect to authorize — it's a static SPA, no server 302) and /connect/callback (validate params, show account + copyable code + safety warning). Both routes guard against non-hosted deployments — redirect to / unless isHostedStaticApp(), same pattern as pair.tsx, since this bundle also ships in local instances. Plus the Clerk dashboard redirect-URI entry.apps/web/src/routes/connect.tsx
apps/web/src/routes/connect.callback.tsx
~200
2CLI out-of-band OAuth flow + single command. Add an out-of-band OAuth login path to CliTokenManager (print URL, Prompt.text for the code, reuse exchangeToken). Make bare t3 connect a handler = login + link (subcommands untouched). Auto-pick headless mode inside SSH sessions (SSH_CONNECTION/SSH_TTY — nothing else); --headless flag as manual override. Loopback stays the default on desktop — no regression.cloud/CliTokenManager.ts (+60)
cloud/publicConfig.ts (+10)
cli/connect.ts (+60)
~150
3Background on boot — Linux first (the SSH case). One new module: pinned runtime install to ~/.t3/runtime/versions/<v> + current symlink, systemd user unit with absolute node/t3 paths, enable-linger. y/n prompt at the end of connect; teardown in logout. Install and service-start failures must land in a log file (under ~/.t3/userdata/logs/) whose path is printed at connect time — systemd user units fail invisibly otherwise. Unit-file generation is pure string-building → trivially testable. macOS launchd / Windows follow as 3b/3c only if wanted.cloud/bootService.ts (new)
cli/connect.ts (+prompt/teardown)
~250
4Web-triggered update (optional follow-up PR; not part of this one, not needed for the core UX). Web detects daemon version < latest-on-channel using the existing version-skew surface + hosted manifest; one authenticated "update" command — client says update, daemon resolves/verifies the version itself (never client-specified — that would be RCE). Stage install → verify → atomic symlink swap → systemctl --user restart. Progress streams reuse the RelayClientInstallProgressEvent pattern.web banner + one control command + daemon update routinelater
+ +

Runtime layout (phase 3)

+
~/.t3/runtime/
+├── versions/0.0.27/        ← npm install --prefix (gets native deps right: node-pty etc.)
+└── current -> versions/0.0.27
+
+~/.config/systemd/user/t3code.service   ← ExecStart=/abs/path/node .../current/.../t3 serve
+loginctl enable-linger $USER            ← survives SSH logout / reboot
+

Why a real npm install and not "reuse the npx binary": the npx cache is ephemeral and t3 ships native deps (node-pty, @ff-labs/fff-node) that need per-platform prebuilds. Why pinned and not npx t3@latest in the unit: a boot-time registry fetch means the box may simply not come up (network down, PATH-less systemd env, nvm). Deterministic boot; updates happen out-of-band (phase 4 follow-up) or by re-running npx t3 connect.

+ +

Explicitly not doing

+
    +
  • Relay / infra / contracts changes — none, in any phase
  • +
  • Short-link service (app.t3.codes/c/AB7K) — only matters for hand-typing; revisit if ever needed
  • +
  • RFC 8628 device grant — wrong UX direction, unverified Clerk support
  • +
  • Auto-update loop in the daemon — web-triggered only (phase 4 follow-up), user stays in control
  • +
  • Project auto-registration — workspace assumed set up; the web UI handles the rest
  • +
  • Changing existing loopback flow, subcommands, or desktop behavior
  • +
+ +

Risks & checks

+
    +
  • Clerk redirect URI: confirm the CLI OAuth client accepts the hosted redirect and that the token endpoint honors PKCE exchange for codes issued to it. Verify in staging before the phase 2 commit. (Only external dependency in the plan.)
  • +
  • systemd user env is minimal: always write absolute paths for node + t3 into the unit; never rely on PATH. Service failures are invisible by default — hence the phase 3 requirement to log to a printed file path.
  • +
  • Linger prompt honesty: the y/n prompt should say the machine becomes reachable via T3 Connect whenever powered on — that's the feature, but say it.
  • +
  • Re-running connect when linked → idempotent: refresh token, re-confirm service, done.
  • +
+ +

Decision log

+
    +
  • Auth: hosted out-of-band OAuth redirect on Clerk PKCE (not relay-brokered pairing, not device grant) — chosen for minimal new surface.
  • +
  • URL: stateless static page, no backend short-link.
  • +
  • Service: real per-user login service (systemd user + linger first); detect + offer install, never silent.
  • +
  • Binary: pinned managed runtime under ~/.t3; interactive npx usage untouched.
  • +
  • Updates: not always-latest; web UI surfaces available updates with one-click trigger (phase 4 follow-up).
  • +
  • Delivery: one PR with a commit per phase (green tree at every boundary), not separate PRs.
  • +
  • Workspace: assumed already set up; no auto-registration.
  • +
+ + + + diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index 5c713ff2be7..16b7a7f715a 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -236,7 +236,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { }), ); - it.effect("logs in to headless connect without enabling access", () => + it.effect("accepts the --headless login override without enabling access", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-cli-cloud-login-test-"), @@ -254,7 +254,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { ); const login = yield* captureStdout( - runConnectCli(["connect", "login", "--base-dir", baseDir]), + runConnectCli(["connect", "login", "--base-dir", baseDir, "--headless"]), ); const status = yield* captureStdout( runConnectCli(["connect", "status", "--base-dir", baseDir, "--json"]), @@ -265,7 +265,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { readonly authenticated: boolean; }; - assert.equal(login.output, "Signed in to T3 Connect."); + assert.equal(login.output, "✓ Signed in"); assert.isFalse(decoded.desired); assert.isTrue(decoded.authenticated); }), diff --git a/apps/server/src/cli/connect.test.ts b/apps/server/src/cli/connect.test.ts index f01c93f0538..e63cbf331b5 100644 --- a/apps/server/src/cli/connect.test.ts +++ b/apps/server/src/cli/connect.test.ts @@ -1,19 +1,45 @@ import * as RelayClient from "@t3tools/shared/relayClient"; import { assert, it } from "@effect/vitest"; import * as Cause from "effect/Cause"; +import * as ConfigProvider from "effect/ConfigProvider"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as References from "effect/References"; +import * as Terminal from "effect/Terminal"; import { acquireRelayClientForLink, + formatHeadlessAuthorizationPrompt, + formatRelayClientReady, + headlessSessionConfig, isPublishAgentActivityEnabledValue, + recoverBootServiceOffer, reportCloudDisconnectResults, } from "./connect.ts"; +it("explains how to complete headless authorization", () => { + assert.equal( + formatHeadlessAuthorizationPrompt("https://example.test/connect"), + [ + "Headless authorization", + "Open this URL on a device with a browser:", + " https://example.test/connect", + "", + "After signing in, return here and enter the code shown in your browser.", + ].join("\n"), + ); +}); + +it("formats relay readiness without printing its installation path", () => { + assert.equal(formatRelayClientReady("2026.5.2"), "✓ Relay client ready · cloudflared 2026.5.2"); +}); + +const readHeadlessSessionConfig = (env: Record) => + headlessSessionConfig.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env })))); + const managedExecutable = { status: "available", executablePath: "/tmp/cloudflared", @@ -21,6 +47,22 @@ const managedExecutable = { version: RelayClient.CLOUDFLARED_VERSION, } as const; +it.effect("detects headless operation from individual SSH config values", () => + Effect.gen(function* () { + assert.isFalse(yield* readHeadlessSessionConfig({})); + assert.isFalse(yield* readHeadlessSessionConfig({ CI: "true" })); + assert.isTrue(yield* readHeadlessSessionConfig({ SSH_CONNECTION: "client server" })); + assert.isTrue(yield* readHeadlessSessionConfig({ SSH_TTY: "/dev/pts/1" })); + }), +); + +it.effect("treats cancelling optional background setup as a successful skip", () => + Effect.gen(function* () { + const result = yield* recoverBootServiceOffer(Effect.fail(new Terminal.QuitError({}))); + assert.isFalse(result); + }), +); + it.effect("does not install the relay client when the user declines the managed download", () => Effect.gen(function* () { let installCalls = 0; diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index d09d0814222..8b11fc73346 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -6,9 +6,12 @@ import { } from "@t3tools/contracts"; import { RelayOkResponse } from "@t3tools/contracts/relay"; import * as RelayClient from "@t3tools/shared/relayClient"; +import * as Terminal from "effect/Terminal"; import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as Cause from "effect/Cause"; +import * as Config from "effect/Config"; import * as Console from "effect/Console"; +import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -16,6 +19,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as References from "effect/References"; +import * as Schema from "effect/Schema"; import { Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli"; import { FetchHttpClient, @@ -25,8 +29,10 @@ import { } from "effect/unstable/http"; import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient"; +import packageJson from "../../package.json" with { type: "json" }; import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; +import * as BootService from "../cloud/bootService.ts"; import * as CliState from "../cloud/CliState.ts"; import * as CliTokenManager from "../cloud/CliTokenManager.ts"; import { @@ -38,6 +44,8 @@ import { relayUrlConfig } from "../cloud/publicConfig.ts"; import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts"; import * as ServerConfig from "../config.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; +import * as ExternalLauncher from "../process/externalLauncher.ts"; +import * as ProcessRunner from "../processRunner.ts"; import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; @@ -46,6 +54,81 @@ const jsonFlag = Flag.boolean("json").pipe( Flag.withDefault(false), ); +const isCloudCliTokenManagerError = Schema.is(CliTokenManager.CloudCliTokenManagerError); + +const headlessFlag = Flag.boolean("headless").pipe( + Flag.withDescription("Authorize without a local browser using out-of-band OAuth."), + Flag.withDefault(false), +); + +/** + * Inside an SSH session there is no local browser to complete the loopback + * OAuth callback, so out-of-band OAuth is the only flow that can work. + */ +export const headlessSessionConfig = Config.all({ + sshConnection: Config.string("SSH_CONNECTION").pipe(Config.option), + sshTty: Config.string("SSH_TTY").pipe(Config.option), +}).pipe( + Config.map(({ sshConnection, sshTty }) => Option.isSome(sshConnection) || Option.isSome(sshTty)), +); + +const promptForOutOfBandOAuthCode = Effect.fn("cloud.cli.prompt_for_out_of_band_oauth_code")( + function* ({ authorizeUrl, validate }: CliTokenManager.OutOfBandOAuthPromptInput) { + yield* Console.log(formatHeadlessAuthorizationPrompt(authorizeUrl)); + return yield* Prompt.run(Prompt.text({ message: "Authorization code", validate })); + }, +); + +export function formatHeadlessAuthorizationPrompt(authorizeUrl: string): string { + return [ + "Headless authorization", + "Open this URL on a device with a browser:", + ` ${authorizeUrl}`, + "", + "After signing in, return here and enter the code shown in your browser.", + ].join("\n"); +} + +/** Returns the connected account identity, if the flow could determine one. */ +const authorizeCli = Effect.fn("cloud.cli.authorize")(function* (options: { + readonly headless: boolean; +}) { + const tokens = yield* CliTokenManager.CloudCliTokenManager; + const useOutOfBandOAuth = options.headless || (yield* headlessSessionConfig); + if (!useOutOfBandOAuth) { + const authorization = yield* tokens.get; + if (authorization._tag === "Authorized") { + return authorization.token.identity ?? null; + } + yield* Console.log("\nHeadless mode enabled. A new authorization link is ready below."); + } + // A stored credential whose refresh fails (revoked, expired grant) must + // fall through to a fresh out-of-band authorization, not dead-end the command. + const existing = yield* tokens.getExisting.pipe( + Effect.catchTag("CloudCliCredentialRefreshError", () => + Console.log( + "The stored T3 Connect credential could not be refreshed; signing in again.", + ).pipe(Effect.as(Option.none())), + ), + ); + if (Option.isSome(existing)) { + return existing.value.identity ?? null; + } + const { token, identity } = yield* CliTokenManager.outOfBandOAuthLogin( + promptForOutOfBandOAuthCode, + ).pipe( + Effect.mapError((cause) => + // Ctrl-C / EOF at the prompt is a QuitError; let it propagate so the CLI + // cancels quietly instead of dumping an authorization error. + Terminal.isQuitError(cause) || isCloudCliTokenManagerError(cause) + ? cause + : new CliTokenManager.CloudCliAuthorizationError({ cause }), + ), + ); + yield* tokens.store(token); + return identity; +}); + function bytesToString(value: Uint8Array): string { return new TextDecoder().decode(value); } @@ -313,6 +396,21 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: { if (options.clearAuthorization) { const tokens = yield* CliTokenManager.CloudCliTokenManager; yield* tokens.clear; + + // uninstall itself no-ops when nothing is installed (and on non-Linux), + // so no status pre-check that could mask a real removal failure. + const bootService = yield* BootService.BootService; + yield* bootService.uninstall.pipe( + Effect.tap((removed) => + removed ? Console.log("Removed the T3 Code background service.") : Effect.void, + ), + Effect.catchTag("BootServiceUnsupportedError", () => Effect.succeed(false)), + Effect.catch((error) => + Console.warn(`Could not remove the background service: ${error.message}`).pipe( + Effect.as(false), + ), + ), + ); } yield* reportCloudDisconnectResults({ @@ -326,7 +424,7 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: { } }); -const runCloudCommand = ( +const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* ( flags: { readonly baseDir: Option.Option }, run: Effect.Effect< A, @@ -335,6 +433,8 @@ const runCloudCommand = ( | CliTokenManager.CloudCliTokenManager | RelayClient.RelayClient | EnvironmentAuth.EnvironmentAuth + | BootService.BootService + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Prompt.Environment @@ -344,37 +444,79 @@ const runCloudCommand = ( options?: { readonly quietLogs?: boolean; }, -) => - Effect.gen(function* () { - const logLevel = yield* GlobalFlag.LogLevel; - const config = yield* resolveCliAuthConfig(flags, logLevel); - const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel; - const runtimeLayer = Layer.mergeAll( - ServerSecretStore.layer, - CliTokenManager.layer.pipe(Layer.provide(ServerSecretStore.layer)), - RelayClient.layerCloudflared({ baseDir: config.baseDir }), - EnvironmentAuth.runtimeLayer, - ServerEnvironment.layer, - headlessRelayClientTracingLayer, - ).pipe( - Layer.provideMerge(FetchHttpClient.layer), - Layer.provideMerge(ServerConfig.layer(config)), - Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), +) { + const logLevel = yield* GlobalFlag.LogLevel; + const config = yield* resolveCliAuthConfig(flags, logLevel); + const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel; + const runtimeLayer = Layer.mergeAll( + ServerSecretStore.layer, + CliTokenManager.layer.pipe( + Layer.provide(ServerSecretStore.layer), + Layer.provide(ExternalLauncher.layer), + ), + RelayClient.layerCloudflared({ baseDir: config.baseDir }), + EnvironmentAuth.runtimeLayer, + ServerEnvironment.layer, + BootService.layer({ + baseDir: config.baseDir, + logsDir: config.logsDir, + cliVersion: packageJson.version, + }).pipe(Layer.provide(ProcessRunner.layer)), + headlessRelayClientTracingLayer, + ).pipe( + Layer.provideMerge(FetchHttpClient.layer), + Layer.provideMerge(ServerConfig.layer(config)), + Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)), + ); + return yield* run.pipe(Effect.provide(runtimeLayer)); +}); + +const connectedAs = (identity: string | null): string => (identity ? ` as ${identity}` : ""); + +export function formatRelayClientReady(version: string): string { + return `✓ Relay client ready · cloudflared ${version}`; +} + +const linkEnvironmentForConnect = Effect.fn("cloud.cli.link_environment")(function* (options: { + readonly headless: boolean; + readonly publishOnly?: boolean; +}) { + const publishOnly = options.publishOnly ?? false; + if (!publishOnly) { + const relayClient = yield* RelayClient.RelayClient; + const installed = yield* acquireRelayClientForLink( + relayClient, + confirmRelayClientInstall, + reportRelayClientInstallProgress, ); - return yield* run.pipe(Effect.provide(runtimeLayer)); - }); + if (Option.isNone(installed)) { + yield* Console.log("T3 Connect setup cancelled. The relay client was not installed."); + return null; + } + yield* Console.log(formatRelayClientReady(installed.value.version)); + } + + const identity = yield* authorizeCli(options); + yield* CliState.setCliDesiredCloudLink(true, publishOnly ? "publish_only" : "managed"); + if (publishOnly) { + const secrets = yield* ServerSecretStore.ServerSecretStore; + yield* secrets.set(PUBLISH_AGENT_ACTIVITY_SECRET, stringToBytes("true")); + } + return { identity } as const; +}); const connectLoginCommand = Command.make("login", { ...projectLocationFlags, + headless: headlessFlag, }).pipe( Command.withDescription("Authorize the T3 Connect CLI without enabling remote access."), Command.withHandler((flags) => runCloudCommand( flags, Effect.gen(function* () { - const tokens = yield* CliTokenManager.CloudCliTokenManager; - yield* tokens.get; - yield* Console.log("Signed in to T3 Connect."); + yield* Console.log("T3 Connect\n"); + const identity = yield* authorizeCli(flags); + yield* Console.log(`✓ Signed in${connectedAs(identity)}`); }), ), ), @@ -382,6 +524,7 @@ const connectLoginCommand = Command.make("login", { const connectLinkCommand = Command.make("link", { ...projectLocationFlags, + headless: headlessFlag, publishOnly: Flag.boolean("publish-only").pipe( Flag.withDescription( "Link to publish agent activity only — no managed tunnel. Reach this environment out of band (e.g. Tailscale).", @@ -394,41 +537,15 @@ const connectLinkCommand = Command.make("link", { runCloudCommand( flags, Effect.gen(function* () { - // A publish-only link needs no Cloudflare tunnel, so skip installing the - // relay client entirely. - if (!flags.publishOnly) { - const relayClient = yield* RelayClient.RelayClient; - const installed = yield* acquireRelayClientForLink( - relayClient, - confirmRelayClientInstall, - reportRelayClientInstallProgress, - ); - if (Option.isNone(installed)) { - yield* Console.log("T3 Connect setup cancelled. The relay client was not installed."); - return; - } + yield* Console.log("T3 Connect\n"); + const linked = yield* linkEnvironmentForConnect(flags); + if (linked) { yield* Console.log( - `Using relay client ${installed.value.version} from ${installed.value.executablePath}.`, + flags.publishOnly + ? `✓ Authorized${connectedAs(linked.identity)}\n\nNext\n Start T3 to publish agent activity (no managed tunnel).` + : `✓ Authorized${connectedAs(linked.identity)}\n\nNext\n Start the server with \`t3 serve\` to make this machine reachable.`, ); } - - const tokens = yield* CliTokenManager.CloudCliTokenManager; - yield* tokens.get; - yield* CliState.setCliDesiredCloudLink( - true, - flags.publishOnly ? "publish_only" : "managed", - ); - if (flags.publishOnly) { - // A publish-only link exists solely to publish; without the publish - // flag the link would be inert and the success message a lie. - const secrets = yield* ServerSecretStore.ServerSecretStore; - yield* secrets.set(PUBLISH_AGENT_ACTIVITY_SECRET, stringToBytes("true")); - } - yield* Console.log( - flags.publishOnly - ? "This environment will publish agent activity to your mobile clients the next time T3 starts (no managed tunnel)." - : "This T3 environment will be available through T3 Connect the next time T3 starts.", - ); }), ), ), @@ -566,8 +683,80 @@ const connectLogoutCommand = Command.make("logout", { ), ); -export const connectCommand = Command.make("connect").pipe( - Command.withDescription("Manage headless T3 Connect access."), +const offerBootService = Effect.gen(function* () { + const bootService = yield* BootService.BootService; + const { supported, installed, current } = yield* bootService.status; + if (!supported) { + // Don't prompt for something that can only fail; background setup is + // Linux/systemd-only for now. + return false; + } + if (installed && current) { + yield* Console.log("T3 Code is already set up to run in the background on this machine."); + return true; + } + const wanted = yield* Prompt.run( + Prompt.confirm({ + message: installed + ? "The installed T3 Code background service is from an older setup. Update it now?" + : "Run T3 Code in the background whenever this machine boots? " + + "It stays reachable through T3 Connect even after you log out.", + initial: true, + }), + ); + if (!wanted) { + return false; + } + const plan = yield* bootService.install; + yield* Console.log(`Background service installed. Logs: ${plan.logPath}`); + return true; +}); + +export const recoverBootServiceOffer = ( + offer: Effect.Effect, +) => + offer.pipe( + Effect.catchTags({ + QuitError: () => Effect.succeed(false), + BootServiceUnsupportedError: (error) => + Console.log(`Skipping background setup: ${error.message}`).pipe(Effect.as(false)), + BootServiceCommandError: (error) => + Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), + BootServiceInstallError: (error) => + Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), + }), + ); + +export const connectCommand = Command.make("connect", { + ...projectLocationFlags, + headless: headlessFlag, +}).pipe( + Command.withDescription("Set up T3 Connect for this machine."), + Command.withHandler((flags) => + runCloudCommand( + flags, + Effect.gen(function* () { + yield* Console.log("T3 Connect\n"); + const linked = yield* linkEnvironmentForConnect(flags); + if (!linked) { + return; + } + // Show which account was linked so an unexpected identity (an + // authorization code for a different account) is visible before the + // machine is brought online. + yield* Console.log(`✓ Connected${connectedAs(linked.identity)}`); + + // Connect itself already succeeded; a boot-service failure must not + // fail the command, just tell the user what happened and move on. + const background = yield* recoverBootServiceOffer(offerBootService); + yield* Console.log( + background + ? "\n✓ Background service ready\n\nT3 Code will stay reachable after you log out." + : "\nNext\n Start the server with `t3 serve` to make this machine reachable.", + ); + }), + ), + ), Command.withSubcommands([ connectLoginCommand, connectLinkCommand, diff --git a/apps/server/src/cloud/CliTokenManager.test.ts b/apps/server/src/cloud/CliTokenManager.test.ts new file mode 100644 index 00000000000..e6eb6b7cd6f --- /dev/null +++ b/apps/server/src/cloud/CliTokenManager.test.ts @@ -0,0 +1,264 @@ +import { readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import * as ConfigProvider from "effect/ConfigProvider"; +import * as Crypto from "effect/Crypto"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Schema from "effect/Schema"; +import * as Terminal from "effect/Terminal"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; + +import * as CliTokenManager from "./CliTokenManager.ts"; +import type { OutOfBandOAuthPromptInput } from "./CliTokenManager.ts"; + +// pk_test_ +const TEST_ENV = { + T3CODE_CLERK_PUBLISHABLE_KEY: "pk_test_Y2xlcmsuZXhhbXBsZS50ZXN0JA==", + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: "oauth_client_test", + T3CODE_HOSTED_APP_URL: "https://hosted.example.test", +}; + +interface RecordedTokenRequest { + readonly url: string; + readonly params: URLSearchParams; +} + +// A JWT whose payload claims { email: "theo@example.test" } (signature is not +// verified — the CLI only reads the claim to display the connected account). +const TestIdTokenHeaderJson = Schema.fromJsonString(Schema.Struct({ alg: Schema.Literal("none") })); +const TestIdTokenPayloadJson = Schema.fromJsonString(Schema.Struct({ email: Schema.String })); +const encodeTestIdTokenHeader = Schema.encodeSync(TestIdTokenHeaderJson); +const encodeTestIdTokenPayload = Schema.encodeSync(TestIdTokenPayloadJson); +const idTokenWithEmail = (() => { + const header = Encoding.encodeBase64Url(encodeTestIdTokenHeader({ alg: "none" })); + const payload = Encoding.encodeBase64Url( + encodeTestIdTokenPayload({ email: "theo@example.test" }), + ); + return `${header}.${payload}.`; +})(); + +const TestTokenResponseJson = Schema.fromJsonString( + Schema.Struct({ + access_token: Schema.String, + refresh_token: Schema.String, + id_token: Schema.String, + expires_in: Schema.Number, + token_type: Schema.String, + }), +); +const encodeTestTokenResponse = Schema.encodeSync(TestTokenResponseJson); + +const makeTokenEndpointLayer = ( + requests: Array, + options?: { readonly idToken?: string }, +) => + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.sync(() => { + const body = + request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : ""; + requests.push({ url: request.url, params: new URLSearchParams(body) }); + return HttpClientResponse.fromWeb( + request, + new Response( + encodeTestTokenResponse({ + access_token: "access-token-1", + refresh_token: "refresh-token-1", + id_token: options?.idToken ?? idTokenWithEmail, + expires_in: 3600, + token_type: "bearer", + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + }), + ), + ); + +const provideTestEnv = Effect.provide( + ConfigProvider.layer(ConfigProvider.fromEnv({ env: TEST_ENV })), +); + +const isAuthorizationError = Schema.is(CliTokenManager.CloudCliAuthorizationError); + +class PromptRejectedError extends Schema.TaggedErrorClass()( + "PromptRejectedError", + { message: Schema.String }, +) {} + +it("formats loopback authorization with a headless-host fallback", () => { + assert.equal( + CliTokenManager.formatLoopbackAuthorizationPrompt("https://clerk.example.test/authorize"), + [ + "Open this URL to authorize T3 Connect:", + " https://clerk.example.test/authorize", + "", + "Press \u001b[1mEnter\u001b[22m to open it in your browser.", + "No browser on this device? Press \u001b[1mH\u001b[22m to switch to headless mode.", + ].join("\n"), + ); +}); + +const makeTestTerminal = (queue: Queue.Queue) => + Terminal.make({ + columns: Effect.succeed(80), + rows: Effect.succeed(24), + readInput: Effect.succeed(Queue.asDequeue(queue)), + readLine: Effect.never, + display: () => Effect.void, + }); + +const userInput = (name: string): Terminal.UserInput => ({ + input: Option.some(name), + key: { name, ctrl: false, meta: false, shift: name !== name.toLowerCase() }, +}); + +it.effect("opens the browser on Enter and switches the active flow on H", () => + Effect.gen(function* () { + const queue = yield* Queue.make(); + yield* Queue.offerAll(queue, [userInput("enter"), userInput("H")]); + const opened: Array = []; + + const result = yield* CliTokenManager.waitForLoopbackAuthorization({ + authorizationUrl: "https://clerk.example.test/authorize", + callback: Effect.never, + terminal: makeTestTerminal(queue), + launchBrowser: (url) => + Effect.sync(() => { + opened.push(url); + }), + }); + + assert.deepEqual(opened, ["https://clerk.example.test/authorize"]); + assert.deepEqual(result, { _tag: "HeadlessRequested" }); + }), +); + +it.effect("finishes normally when the browser callback wins", () => + Effect.gen(function* () { + const queue = yield* Queue.make(); + const callback = yield* Deferred.make(); + yield* Deferred.succeed(callback, "clerk-code-123"); + + const result = yield* CliTokenManager.waitForLoopbackAuthorization({ + authorizationUrl: "https://clerk.example.test/authorize", + callback: Deferred.await(callback), + terminal: makeTestTerminal(queue), + launchBrowser: () => Effect.die("browser launch should not run"), + }); + + assert.deepEqual(result, { _tag: "AuthorizationCode", code: "clerk-code-123" }); + }), +); + +it.layer(NodeServices.layer)("CliTokenManager.outOfBandOAuthLogin", (it) => { + it.effect("prints a hosted authorize URL and exchanges the out-of-band code with PKCE", () => + Effect.gen(function* () { + const requests: Array = []; + let seenAuthorizeUrl = ""; + + const { token, identity } = yield* CliTokenManager.outOfBandOAuthLogin( + ({ authorizeUrl, validate }: OutOfBandOAuthPromptInput) => + Effect.gen(function* () { + seenAuthorizeUrl = authorizeUrl; + const request = readConnectAuthorizeRequest(new URL(authorizeUrl)); + assert.isNotNull(request); + return yield* validate(`clerk-code-123.${request!.state}`).pipe( + Effect.mapError((message) => new PromptRejectedError({ message })), + ); + }), + ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv); + + const authorizeUrl = new URL(seenAuthorizeUrl); + assert.equal(authorizeUrl.origin, "https://hosted.example.test"); + assert.equal(authorizeUrl.pathname, "/connect"); + const request = readConnectAuthorizeRequest(authorizeUrl); + assert.isNotNull(request); + assert.match(request!.state, /^[A-Za-z0-9_-]{22}$/); + + assert.equal(token.accessToken, "access-token-1"); + assert.equal(token.refreshToken, "refresh-token-1"); + assert.equal(token.identity, "theo@example.test"); + // The id_token's email claim is surfaced so connect can show the account. + assert.equal(identity, "theo@example.test"); + + assert.lengthOf(requests, 1); + const exchange = requests[0]!; + assert.equal(exchange.url, "https://clerk.example.test/oauth/token"); + assert.equal(exchange.params.get("grant_type"), "authorization_code"); + assert.equal(exchange.params.get("code"), "clerk-code-123"); + assert.equal( + exchange.params.get("redirect_uri"), + "https://hosted.example.test/connect/callback", + ); + assert.equal(exchange.params.get("client_id"), "oauth_client_test"); + // The verifier must hash to the challenge advertised in the authorize URL. + const verifier = exchange.params.get("code_verifier"); + assert.isNotNull(verifier); + const crypto = yield* Crypto.Crypto; + const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier!)); + assert.equal(Encoding.encodeBase64Url(digest), request!.challenge); + }), + ); + + it.effect("rejects out-of-band codes whose state does not match the request", () => + Effect.gen(function* () { + const requests: Array = []; + + const validationErrors: Array = []; + const result = yield* CliTokenManager.outOfBandOAuthLogin( + ({ validate }: OutOfBandOAuthPromptInput) => + validate("clerk-code-123.wrong-state").pipe( + Effect.tapError((message) => Effect.sync(() => validationErrors.push(message))), + Effect.mapError((message) => new PromptRejectedError({ message })), + ), + ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip); + + assert.lengthOf(requests, 0); + assert.lengthOf(validationErrors, 1); + assert.include(validationErrors[0], "different connect request"); + assert.instanceOf(result, PromptRejectedError); + }), + ); + + it.effect("ignores an id_token whose claims are not valid JSON", () => + Effect.gen(function* () { + const requests: Array = []; + const malformedIdToken = `header.${Encoding.encodeBase64Url("not-json")}.signature`; + + const { identity } = yield* CliTokenManager.outOfBandOAuthLogin( + ({ authorizeUrl }: OutOfBandOAuthPromptInput) => { + const request = readConnectAuthorizeRequest(new URL(authorizeUrl)); + assert.isNotNull(request); + return Effect.succeed(`clerk-code-123.${request!.state}`); + }, + ).pipe( + Effect.provide(makeTokenEndpointLayer(requests, { idToken: malformedIdToken })), + provideTestEnv, + ); + + assert.isNull(identity); + assert.lengthOf(requests, 1); + }), + ); + + it.effect("fails without touching the token endpoint when the prompt returns garbage", () => + Effect.gen(function* () { + const requests: Array = []; + + const result = yield* CliTokenManager.outOfBandOAuthLogin(() => + Effect.succeed("not-a-connect-code"), + ).pipe(Effect.provide(makeTokenEndpointLayer(requests)), provideTestEnv, Effect.flip); + + assert.lengthOf(requests, 0); + assert.isTrue(isAuthorizationError(result)); + }), + ); +}); diff --git a/apps/server/src/cloud/CliTokenManager.ts b/apps/server/src/cloud/CliTokenManager.ts index 00709370b26..f01599bb96f 100644 --- a/apps/server/src/cloud/CliTokenManager.ts +++ b/apps/server/src/cloud/CliTokenManager.ts @@ -3,6 +3,7 @@ import * as NodeHttp from "node:http"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as Clock from "effect/Clock"; +import * as Cause from "effect/Cause"; import * as Console from "effect/Console"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; @@ -12,8 +13,10 @@ import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; +import * as Terminal from "effect/Terminal"; import * as HttpClient from "effect/unstable/http/HttpClient"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; @@ -21,19 +24,105 @@ import * as HttpRouter from "effect/unstable/http/HttpRouter"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { + buildConnectAuthorizeRequestUrl, + buildConnectClerkAuthorizeUrl, + checkConnectAuthCode, + connectCallbackUrl, +} from "@t3tools/shared/connectAuth"; + import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; -import { cloudCliOAuthConfig, type CloudCliOAuthConfig } from "./publicConfig.ts"; +import * as ExternalLauncher from "../process/externalLauncher.ts"; +import { + cloudCliOAuthConfig, + hostedAppUrlConfig, + type CloudCliOAuthConfig, +} from "./publicConfig.ts"; +import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts"; const CLOUD_CLI_OAUTH_TOKEN_SECRET = "cloud-cli-oauth-token"; const CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT = Duration.minutes(10); const CLOUD_CLI_OAUTH_REFRESH_EARLY_MS = Duration.toMillis(Duration.minutes(5)); +const boldTerminalText = (value: string): string => `\u001b[1m${value}\u001b[22m`; + +export function formatLoopbackAuthorizationPrompt(authorizationUrl: string): string { + return [ + "Open this URL to authorize T3 Connect:", + ` ${authorizationUrl}`, + "", + `Press ${boldTerminalText("Enter")} to open it in your browser.`, + `No browser on this device? Press ${boldTerminalText("H")} to switch to headless mode.`, + ].join("\n"); +} + +export type LoopbackAuthorizationResult = + | { readonly _tag: "AuthorizationCode"; readonly code: string } + | { readonly _tag: "HeadlessRequested" }; + +const readLoopbackAuthorizationAction = Effect.fn( + "cloud.cli_token.read_loopback_authorization_action", +)(function* (input: Queue.Dequeue) { + while (true) { + const event = yield* Queue.take(input).pipe(Effect.mapError(() => new Terminal.QuitError({}))); + const keyName = event.key.name.toLowerCase(); + if (!event.key.ctrl && !event.key.meta && keyName === "h") { + return "headless" as const; + } + if (keyName === "enter" || keyName === "return") { + return "open-browser" as const; + } + } +}); + +export const waitForLoopbackAuthorization = Effect.fn( + "cloud.cli_token.wait_for_loopback_authorization", +)(function* (input: { + readonly authorizationUrl: string; + readonly callback: Effect.Effect; + readonly terminal: Terminal.Terminal; + readonly launchBrowser: ( + url: string, + ) => Effect.Effect; +}) { + return yield* Effect.scoped( + Effect.gen(function* () { + const terminalInput = yield* input.terminal.readInput; + while (true) { + const result = yield* Effect.raceFirst( + input.callback.pipe( + Effect.map( + (code): LoopbackAuthorizationResult => ({ _tag: "AuthorizationCode", code }), + ), + ), + readLoopbackAuthorizationAction(terminalInput), + ); + if (typeof result !== "string") { + return result; + } + if (result === "headless") { + return { _tag: "HeadlessRequested" } as const; + } + yield* input + .launchBrowser(input.authorizationUrl) + .pipe( + Effect.catch(() => + Console.warn( + `Could not open a browser on this device. Open the URL above manually, or press ${boldTerminalText("H")} to switch to headless mode.`, + ), + ), + ); + } + }), + ); +}); const PersistedToken = Schema.Struct({ accessToken: Schema.String, refreshToken: Schema.String, expiresAtEpochMs: Schema.Number, + identity: Schema.optional(Schema.String), }); -type PersistedToken = typeof PersistedToken.Type; +export type PersistedToken = typeof PersistedToken.Type; const PersistedTokenJson = Schema.fromJsonString(PersistedToken); const decodePersistedToken = Schema.decodeUnknownEffect(PersistedTokenJson); @@ -42,10 +131,39 @@ const encodePersistedToken = Schema.encodeEffect(PersistedTokenJson); const OAuthTokenResponse = Schema.Struct({ access_token: Schema.String, refresh_token: Schema.optional(Schema.String), + id_token: Schema.optional(Schema.String), expires_in: Schema.Number, token_type: Schema.String, }); +const OidcIdentityClaimsJson = Schema.fromJsonString( + Schema.Struct({ + email: Schema.optional(Schema.String), + preferred_username: Schema.optional(Schema.String), + sub: Schema.optional(Schema.String), + }), +); +const decodeOidcIdentityClaimsJson = Schema.decodeUnknownOption(OidcIdentityClaimsJson); + +/** + * Best-effort read of the `email` (or fallback) claim from an OIDC id_token. + * Only used to show the operator which account they linked, so a malformed + * token degrades to "no identity" rather than an error. + */ +function idTokenIdentity(idToken: string | undefined): string | null { + if (!idToken) return null; + const payload = idToken.split(".")[1]; + if (!payload) return null; + const decoded = Encoding.decodeBase64UrlString(payload); + if (decoded._tag !== "Success") return null; + const claims = decodeOidcIdentityClaimsJson(decoded.success); + if (Option.isNone(claims)) return null; + for (const value of [claims.value.email, claims.value.preferred_username, claims.value.sub]) { + if (typeof value === "string" && value.length > 0) return value; + } + return null; +} + export class CloudCliCredentialRemovalError extends Schema.TaggedErrorClass()( "CloudCliCredentialRemovalError", { cause: Schema.Defect() }, @@ -103,18 +221,18 @@ export type CloudCliTokenManagerError = typeof CloudCliTokenManagerError.Type; export class CloudCliTokenManager extends Context.Service< CloudCliTokenManager, { - readonly get: Effect.Effect; + readonly get: Effect.Effect< + | { readonly _tag: "Authorized"; readonly token: PersistedToken } + | { readonly _tag: "HeadlessRequested" }, + CloudCliTokenManagerError | Terminal.QuitError + >; readonly getExisting: Effect.Effect, CloudCliTokenManagerError>; readonly hasCredential: Effect.Effect; + readonly store: (token: PersistedToken) => Effect.Effect; readonly clear: Effect.Effect; } >()("t3/cloud/CliTokenManager/CloudCliTokenManager") {} -const wrapError = - (makeError: (cause: unknown) => WrappedError) => - (effect: Effect.Effect): Effect.Effect => - effect.pipe(Effect.mapError(makeError)); - function stringToBytes(value: string): Uint8Array { return new TextEncoder().encode(value); } @@ -123,10 +241,101 @@ function bytesToString(value: Uint8Array): string { return new TextDecoder().decode(value); } +const exchangeToken = Effect.fn("cloud.cli_token.exchange")(function* ( + metadata: Pick, + params: Record, +) { + const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk); + const response = yield* HttpClientRequest.post(metadata.tokenEndpoint).pipe( + HttpClientRequest.bodyUrlParams(params), + httpClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthTokenResponse)), + ); + const now = yield* Clock.currentTimeMillis; + const identity = idTokenIdentity(response.id_token); + return { + token: { + accessToken: response.access_token, + refreshToken: response.refresh_token ?? params.refresh_token ?? "", + expiresAtEpochMs: now + response.expires_in * 1_000, + ...(identity === null ? {} : { identity }), + } satisfies PersistedToken, + identity, + }; +}); + +const makePkceRequest = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32)); + const challenge = Encoding.encodeBase64Url( + yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier)), + ); + const state = Encoding.encodeBase64Url(yield* crypto.randomBytes(16)); + return { verifier, challenge, state }; +}); + +export interface OutOfBandOAuthPromptInput { + readonly authorizeUrl: string; + readonly validate: (value: string) => Effect.Effect; +} + +/** + * Out-of-band OAuth for machines without a local browser (SSH). The user + * opens the hosted /connect URL elsewhere, signs in, and enters the displayed + * code in this terminal. The PKCE verifier never leaves this process, so the + * authorization code is useless to an observer, and the state bundled into + * the blob preserves the loopback flow's CSRF check. + */ +export const outOfBandOAuthLogin = Effect.fn("cloud.cli_token.out_of_band_oauth_login")(function* < + E, + R, +>(promptForCode: (input: OutOfBandOAuthPromptInput) => Effect.Effect) { + const metadata = yield* cloudCliOAuthConfig; + const hostedAppUrl = yield* hostedAppUrlConfig; + const { verifier, challenge, state } = yield* makePkceRequest; + + const authorizationCode = yield* promptForCode({ + authorizeUrl: buildConnectAuthorizeRequestUrl({ hostedAppUrl, state, challenge }), + validate: (value) => { + const checked = checkConnectAuthCode(value, state); + return typeof checked === "string" ? Effect.fail(checked) : Effect.succeed(value); + }, + }).pipe( + // Clerk authorization codes expire on this horizon anyway; matching the + // loopback flow's timeout turns an abandoned prompt into a clear error. + Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT), + Effect.catchTag("TimeoutError", (cause) => + Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), + ), + ); + // promptForCode is caller-supplied, so re-check the returned value rather + // than trusting that the prompt ran validate. + const authCode = checkConnectAuthCode(authorizationCode, state); + if (typeof authCode === "string") { + return yield* new CloudCliAuthorizationError({ cause: authCode }); + } + + return yield* exchangeToken(metadata, { + grant_type: "authorization_code", + code: authCode.code, + redirect_uri: connectCallbackUrl(hostedAppUrl), + client_id: metadata.clientId, + code_verifier: verifier, + }); +}); + export const make = Effect.gen(function* () { + // Capture exactly the services the login/refresh flows need at build time + // (matching the behavior before the out-of-band flow captured the instances), not + // the whole ambient context. const crypto = yield* Crypto.Crypto; - const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk); + const httpClient = yield* HttpClient.HttpClient; + const services = Context.make(Crypto.Crypto, crypto).pipe( + Context.add(HttpClient.HttpClient, httpClient), + ); const secrets = yield* ServerSecretStore.ServerSecretStore; + const terminal = yield* Terminal.Terminal; + const externalLauncher = yield* ExternalLauncher.ExternalLauncher; const semaphore = yield* Semaphore.make(1); const persist = Effect.fn("cloud.cli_token.persist")(function* (token: PersistedToken) { const encoded = yield* encodePersistedToken(token); @@ -136,7 +345,7 @@ export const make = Effect.gen(function* () { const clear = secrets .remove(CLOUD_CLI_OAUTH_TOKEN_SECRET) - .pipe(wrapError((cause) => new CloudCliCredentialRemovalError({ cause }))); + .pipe(Effect.mapError((cause) => new CloudCliCredentialRemovalError({ cause }))); const read = Effect.fn("cloud.cli_token.read")(function* () { const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET); @@ -144,39 +353,21 @@ export const make = Effect.gen(function* () { return Option.some(yield* decodePersistedToken(bytesToString(encoded.value))); }); - const exchangeToken = Effect.fn("cloud.cli_token.exchange")(function* ( - metadata: CloudCliOAuthConfig, - params: Record, - ) { - const response = yield* HttpClientRequest.post(metadata.tokenEndpoint).pipe( - HttpClientRequest.bodyUrlParams(params), - httpClient.execute, - Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthTokenResponse)), - ); - const now = yield* Clock.currentTimeMillis; - return { - accessToken: response.access_token, - refreshToken: response.refresh_token ?? params.refresh_token ?? "", - expiresAtEpochMs: now + response.expires_in * 1_000, - } satisfies PersistedToken; - }); - const refresh = Effect.fn("cloud.cli_token.refresh")(function* (token: PersistedToken) { const metadata = yield* cloudCliOAuthConfig; - return yield* exchangeToken(metadata, { + const { token: refreshed } = yield* exchangeToken(metadata, { grant_type: "refresh_token", refresh_token: token.refreshToken, client_id: metadata.clientId, }); + return refreshed.identity === undefined && token.identity !== undefined + ? { ...refreshed, identity: token.identity } + : refreshed; }); const login = Effect.fn("cloud.cli_token.login")(function* () { const metadata = yield* cloudCliOAuthConfig; - const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32)); - const challenge = Encoding.encodeBase64Url( - yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier)), - ); - const state = yield* crypto.randomUUIDv4; + const { verifier, challenge, state } = yield* makePkceRequest; const callback = yield* Deferred.make(); const callbackRoute = HttpRouter.add( "GET", @@ -191,14 +382,7 @@ export const make = Effect.gen(function* () { }); } yield* Deferred.succeed(callback, code); - return yield* HttpServerResponse.html` - - -

T3 Connect authorization complete

-

You can close this window and return to your terminal.

- - -`; + return HttpServerResponse.html(renderLoopbackAuthorizationCompleteHtml()); }), ); yield* HttpRouter.serve(callbackRoute, { @@ -214,32 +398,37 @@ export const make = Effect.gen(function* () { ), Layer.build, ); - const authorizationUrl = new URL(metadata.authorizationEndpoint); - authorizationUrl.searchParams.set("client_id", metadata.clientId); - authorizationUrl.searchParams.set("redirect_uri", metadata.redirectUri); - authorizationUrl.searchParams.set("response_type", "code"); - authorizationUrl.searchParams.set("scope", metadata.scopes.join(" ")); - authorizationUrl.searchParams.set("state", state); - authorizationUrl.searchParams.set("code_challenge", challenge); - authorizationUrl.searchParams.set("code_challenge_method", "S256"); - yield* Console.log(`Open this URL to authorize T3 Connect:\n${authorizationUrl.toString()}\n`); - const code = yield* Deferred.await(callback).pipe( - Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT), - Effect.catchTag("TimeoutError", (cause) => - Effect.fail( - new CloudCliAuthorizationTimeoutError({ - cause, - }), + const authorizationUrl = buildConnectClerkAuthorizeUrl({ + authorizationEndpoint: metadata.authorizationEndpoint, + clientId: metadata.clientId, + redirectUri: metadata.redirectUri, + scopes: metadata.scopes, + state, + challenge, + }); + yield* Console.log(formatLoopbackAuthorizationPrompt(authorizationUrl)); + const authorization = yield* waitForLoopbackAuthorization({ + authorizationUrl, + callback: Deferred.await(callback).pipe( + Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT), + Effect.catchTag("TimeoutError", (cause) => + Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })), ), ), - ); - return yield* exchangeToken(metadata, { + terminal, + launchBrowser: externalLauncher.launchBrowser, + }); + if (authorization._tag === "HeadlessRequested") { + return authorization; + } + const { token } = yield* exchangeToken(metadata, { grant_type: "authorization_code", - code, + code: authorization.code, redirect_uri: metadata.redirectUri, client_id: metadata.clientId, code_verifier: verifier, }); + return { _tag: "Authorized", token } as const; }); const getExistingNoLock = Effect.fn("cloud.cli_token.get_existing_no_lock")(function* () { @@ -253,24 +442,50 @@ export const make = Effect.gen(function* () { }); const getExisting = semaphore.withPermits(1)( - getExistingNoLock().pipe(wrapError((cause) => new CloudCliCredentialRefreshError({ cause }))), + getExistingNoLock().pipe( + Effect.mapError((cause) => new CloudCliCredentialRefreshError({ cause })), + Effect.provide(services), + ), ); const hasCredential = semaphore.withPermits(1)( read().pipe( Effect.map(Option.isSome), - wrapError((cause) => new CloudCliCredentialReadError({ cause })), + Effect.mapError((cause) => new CloudCliCredentialReadError({ cause })), ), ); const get = semaphore.withPermits(1)( Effect.gen(function* () { - const token = yield* getExistingNoLock(); - return Option.isSome(token) - ? token.value - : yield* Effect.scoped(login()).pipe(Effect.flatMap(persist)); - }).pipe(wrapError((cause) => new CloudCliAuthorizationError({ cause }))), + // A stored credential that can't be read or refreshed (corrupt, revoked, + // expired grant) must fall through to a fresh login rather than dead-end + // the command — authorizeCli applies the same fallback to out-of-band + // authorization. + const token = yield* getExistingNoLock().pipe( + Effect.orElseSucceed(() => Option.none()), + ); + if (Option.isSome(token)) { + return { _tag: "Authorized", token: token.value } as const; + } + const authorization = yield* Effect.scoped(login()); + return authorization._tag === "Authorized" + ? ({ _tag: "Authorized", token: yield* persist(authorization.token) } as const) + : authorization; + }).pipe( + Effect.mapError((cause) => + Terminal.isQuitError(cause) ? cause : new CloudCliAuthorizationError({ cause }), + ), + Effect.provide(services), + ), ); + const store = Effect.fn("cloud.cli_token.store")(function* (token: PersistedToken) { + yield* semaphore.withPermits(1)( + persist(token).pipe( + Effect.asVoid, + Effect.mapError((cause) => new CloudCliAuthorizationError({ cause })), + ), + ); + }); - return CloudCliTokenManager.of({ get, getExisting, hasCredential, clear }); + return CloudCliTokenManager.of({ get, getExisting, hasCredential, store, clear }); }); export const layer = Layer.effect(CloudCliTokenManager, make); diff --git a/apps/server/src/cloud/bootService.test.ts b/apps/server/src/cloud/bootService.test.ts new file mode 100644 index 00000000000..a24d54697d9 --- /dev/null +++ b/apps/server/src/cloud/bootService.test.ts @@ -0,0 +1,529 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import * as ConfigProvider from "effect/ConfigProvider"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; + +import { + HostProcessArguments, + HostProcessExecutablePath, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; + +import * as ProcessRunner from "../processRunner.ts"; +import * as BootService from "./bootService.ts"; + +const isUnsupportedError = Schema.is(BootService.BootServiceUnsupportedError); +const isCommandError = Schema.is(BootService.BootServiceCommandError); + +interface RecordedCommand { + readonly command: string; + readonly args: ReadonlyArray; +} + +const makeRecordingRunnerLayer = ( + commands: Array, + options?: { + readonly failCommand?: string; + readonly failWhen?: (command: string, args: ReadonlyArray) => boolean; + }, +) => + Layer.succeed( + ProcessRunner.ProcessRunner, + ProcessRunner.ProcessRunner.of({ + run: (input) => + Effect.sync(() => { + assert.isUndefined(input.env); + commands.push({ command: input.command, args: input.args }); + const failed = + input.command === options?.failCommand || + options?.failWhen?.(input.command, input.args) === true; + return { + stdout: "", + stderr: failed ? `${input.command} exploded` : "", + code: ChildProcessSpawner.ExitCode(failed ? 1 : 0), + timedOut: false, + stdoutTruncated: false, + stderrTruncated: false, + }; + }), + }), + ); + +const makeHost = (entry: string): BootService.BootServiceHost => ({ + execPath: "/usr/local/bin/node", + cliEntryPath: entry, +}); + +const provideHostRefs = (home: string, platform: NodeJS.Platform = "linux") => + Effect.provide( + Layer.mergeAll( + Layer.succeed(HostProcessPlatform, platform), + ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })), + ), + ); + +const makeTestContext = Effect.fn("test.makeTestContext")(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-boot-service-test-" }); + // A real file for the stable-entry cases so status can confirm the entry + // point exists. + const stableEntry = path.join(root, "bin.mjs"); + yield* fs.writeFileString(stableEntry, "#!/usr/bin/env node\n"); + return { + fs, + path, + dirs: { + home: root, + baseDir: path.join(root, ".t3"), + logsDir: path.join(root, ".t3", "userdata", "logs"), + stableEntry, + }, + }; +}); + +it("renders a systemd unit with absolute paths and append-mode logging", () => { + const unit = BootService.renderBootServiceUnit({ + nodePath: "/usr/local/bin/node", + t3EntryPath: "/home/theo/.t3/runtime/versions/0.0.27/node_modules/t3/dist/bin.mjs", + baseDir: "/home/theo/.t3", + logPath: "/home/theo/.t3/userdata/logs/boot-service.log", + unitPath: "/home/theo/.config/systemd/user/t3code.service", + }); + + assert.equal( + unit, + [ + "[Unit]", + "Description=T3 Code server (T3 Connect)", + "StartLimitIntervalSec=300", + "StartLimitBurst=5", + "", + "[Service]", + "Type=simple", + "WorkingDirectory=%h", + "Environment=T3CODE_HOME=/home/theo/.t3", + "ExecStart=/usr/local/bin/node /home/theo/.t3/runtime/versions/0.0.27/node_modules/t3/dist/bin.mjs serve", + "Restart=always", + "RestartSec=5", + "StandardOutput=append:/home/theo/.t3/userdata/logs/boot-service.log", + "StandardError=append:/home/theo/.t3/userdata/logs/boot-service.log", + "", + "[Install]", + "WantedBy=default.target", + "", + ].join("\n"), + ); +}); + +it("quotes systemd values containing spaces and escapes percent specifiers", () => { + assert.equal(BootService.quoteSystemdValue("/plain/path"), "/plain/path"); + assert.equal(BootService.quoteSystemdValue("/home/me/T3 Data"), '"/home/me/T3 Data"'); + assert.equal(BootService.quoteSystemdValue("/opt/100%cpu"), "/opt/100%%cpu"); + + const unit = BootService.renderBootServiceUnit({ + nodePath: "/home/me/my tools/node", + t3EntryPath: "/home/me/T3 Data/bin.mjs", + baseDir: "/home/me/T3 Data", + logPath: "/home/me/100%logs/boot.log", + unitPath: "/home/me/.config/systemd/user/t3code.service", + }); + assert.include(unit, 'ExecStart="/home/me/my tools/node" "/home/me/T3 Data/bin.mjs" serve'); + assert.include(unit, 'Environment=T3CODE_HOME="/home/me/T3 Data"'); + // append: paths take the rest of the line literally (spaces are fine, + // quoting is not), but % still goes through specifier expansion. + assert.include(unit, "StandardOutput=append:/home/me/100%%logs/boot.log"); + assert.include(unit, "StandardError=append:/home/me/100%%logs/boot.log"); +}); + +it("flags package-manager cache entry points as ephemeral", () => { + assert.isTrue( + BootService.isEphemeralCacheEntry("/home/theo/.npm/_npx/abc123/node_modules/t3/dist/bin.mjs"), + ); + assert.isTrue( + BootService.isEphemeralCacheEntry("C:\\Users\\theo\\AppData\\npm-cache\\_npx\\abc\\bin.mjs"), + ); + assert.isTrue( + BootService.isEphemeralCacheEntry( + "/home/theo/.cache/pnpm/dlx/abc/node_modules/t3/dist/bin.mjs", + ), + ); + assert.isTrue( + BootService.isEphemeralCacheEntry("/home/theo/.bun/install/cache/t3@0.0.27/dist/bin.mjs"), + ); + assert.isFalse(BootService.isEphemeralCacheEntry("/usr/local/lib/node_modules/t3/dist/bin.mjs")); + assert.isFalse( + BootService.isEphemeralCacheEntry( + "/home/theo/dev/pnpm/dlx-tools/t3/node_modules/t3/dist/bin.mjs", + ), + ); + assert.isFalse( + BootService.isEphemeralCacheEntry( + "/home/theo/.t3/runtime/versions/0.0.27/node_modules/t3/dist/bin.mjs", + ), + ); +}); + +it.layer(NodeServices.layer)("BootService", (it) => { + it.effect("installs the unit, enables the service, and enables linger", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe(Effect.provide(makeRecordingRunnerLayer(commands)), provideHostRefs(dirs.home)); + + const plan = yield* service.install; + + // A stable entry point is reused directly — no npm install. + assert.equal(plan.t3EntryPath, dirs.stableEntry); + assert.deepEqual( + commands.map((entry) => [entry.command, ...entry.args].join(" ")), + [ + "systemctl --user daemon-reload", + "systemctl --user enable t3code.service", + // restart (not enable --now) so repairing a stale unit replaces a + // running process instead of leaving the old one until reboot. + "systemctl --user restart t3code.service", + "loginctl enable-linger", + ], + ); + + const unitPath = path.join(dirs.home, ".config", "systemd", "user", "t3code.service"); + const unit = yield* fs.readFileString(unitPath); + assert.include(unit, `ExecStart=/usr/local/bin/node ${dirs.stableEntry} serve`); + assert.include(unit, `Environment=T3CODE_HOME=${dirs.baseDir}`); + + const status = yield* service.status; + assert.isTrue(status.supported); + assert.isTrue(status.installed); + assert.isTrue(status.current); + + const removed = yield* service.uninstall; + assert.isTrue(removed); + assert.isFalse(yield* fs.exists(unitPath)); + const statusAfter = yield* service.status; + assert.isFalse(statusAfter.installed); + const removedAgain = yield* service.uninstall; + assert.isFalse(removedAgain); + }), + ); + + it.effect("pins a runtime via npm install when running from the npx cache", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/home/theo/.npm/_npx/abc/node_modules/t3/dist/bin.mjs"), + }).pipe(Effect.provide(makeRecordingRunnerLayer(commands)), provideHostRefs(dirs.home)); + + const plan = yield* service.install; + + const runtimeDir = path.join(dirs.baseDir, "runtime", "versions", "0.0.27"); + assert.equal( + plan.t3EntryPath, + path.join(runtimeDir, "node_modules", "t3", "dist", "bin.mjs"), + ); + assert.deepEqual(commands[0], { + command: "npm", + args: ["install", "--prefix", runtimeDir, "--no-fund", "--no-audit", "t3@0.0.27"], + }); + // Success is recorded via a sentinel so interrupted installs re-run. + assert.isTrue(yield* fs.exists(path.join(runtimeDir, ".install-complete"))); + }), + ); + + it.effect("reinstalls a pinned runtime when its entry point is missing", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/home/theo/.npm/_npx/abc/node_modules/t3/dist/bin.mjs"), + }).pipe(Effect.provide(makeRecordingRunnerLayer(commands)), provideHostRefs(dirs.home)); + + const plan = yield* service.install; + yield* fs.makeDirectory(path.dirname(plan.t3EntryPath), { recursive: true }); + yield* fs.writeFileString(plan.t3EntryPath, "#!/usr/bin/env node\n"); + yield* fs.remove(plan.t3EntryPath); + commands.length = 0; + + yield* service.install; + + assert.isTrue(commands.some(({ command }) => command === "npm")); + }), + ); + + it.effect("reads executable metadata from host process references", () => + Effect.gen(function* () { + const { dirs } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + }).pipe( + Effect.provide(makeRecordingRunnerLayer(commands)), + provideHostRefs(dirs.home), + Effect.provideService(HostProcessExecutablePath, "/opt/node/bin/node"), + Effect.provideService(HostProcessArguments, ["/opt/node/bin/node", dirs.stableEntry]), + ); + + const plan = yield* service.install; + assert.equal(plan.nodePath, "/opt/node/bin/node"); + assert.equal(plan.t3EntryPath, dirs.stableEntry); + }), + ); + + it.effect("cleans up and fails when the pinned runtime install fails", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/home/theo/.npm/_npx/abc/node_modules/t3/dist/bin.mjs"), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(commands, { failCommand: "npm" })), + provideHostRefs(dirs.home), + ); + + const error = yield* service.install.pipe(Effect.flip); + assert.isTrue(isCommandError(error)); + const runtimeDir = path.join(dirs.baseDir, "runtime", "versions", "0.0.27"); + // The half-installed tree must not be reused by the next attempt. + assert.isFalse(yield* fs.exists(runtimeDir)); + assert.isFalse(yield* fs.exists(path.join(runtimeDir, ".install-complete"))); + }), + ); + + it.effect("reports an installed-but-stale unit so connect can offer a repair", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe(Effect.provide(makeRecordingRunnerLayer(commands)), provideHostRefs(dirs.home)); + + const unitDir = path.join(dirs.home, ".config", "systemd", "user"); + yield* fs.makeDirectory(unitDir, { recursive: true }); + yield* fs.writeFileString( + path.join(unitDir, "t3code.service"), + "[Service]\nExecStart=/old/node /old/t3 serve\n", + ); + + const status = yield* service.status; + assert.isTrue(status.supported); + assert.isTrue(status.installed); + assert.isFalse(status.current); + }), + ); + + it.effect("reports a current unit as stale when its entry point is gone", () => + Effect.gen(function* () { + const { dirs, fs } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe(Effect.provide(makeRecordingRunnerLayer(commands)), provideHostRefs(dirs.home)); + + yield* service.install; + assert.isTrue((yield* service.status).current); + + // The pinned runtime (or global bin) was deleted to reclaim space; the + // unit still matches byte-for-byte but would crashloop at boot. + yield* fs.remove(dirs.stableEntry); + const status = yield* service.status; + assert.isTrue(status.installed); + assert.isFalse(status.current); + }), + ); + + it.effect("fails on non-Linux platforms without touching the filesystem", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/usr/local/lib/node_modules/t3/dist/bin.mjs"), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(commands)), + provideHostRefs(dirs.home, "darwin"), + ); + + const error = yield* service.install.pipe(Effect.flip); + assert.isTrue(isUnsupportedError(error)); + assert.lengthOf(commands, 0); + assert.isFalse( + yield* fs.exists(path.join(dirs.home, ".config", "systemd", "user", "t3code.service")), + ); + + const status = yield* service.status; + assert.isFalse(status.supported); + assert.isFalse(status.installed); + }), + ); + + it.effect("removes the unit file when an activation step fails", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/usr/local/lib/node_modules/t3/dist/bin.mjs"), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(commands, { failCommand: "loginctl" })), + provideHostRefs(dirs.home), + ); + + const error = yield* service.install.pipe(Effect.flip); + assert.isTrue(isCommandError(error)); + // A leftover unit would make the next connect report "already set up" + // even though linger never happened. + assert.isFalse( + yield* fs.exists(path.join(dirs.home, ".config", "systemd", "user", "t3code.service")), + ); + const status = yield* service.status; + assert.isFalse(status.installed); + assert.isTrue( + commands.some( + ({ command, args }) => + command === "systemctl" && args.join(" ") === "--user disable --now t3code.service", + ), + ); + }), + ); + + it.effect("restores the previous unit when a repair cannot activate", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const initialCommands: Array = []; + const initialService = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(initialCommands)), + provideHostRefs(dirs.home), + ); + yield* initialService.install; + + const unitPath = path.join(dirs.home, ".config", "systemd", "user", "t3code.service"); + const previousUnit = yield* fs.readFileString(unitPath); + const replacementEntry = path.join(dirs.home, "replacement-bin.mjs"); + yield* fs.writeFileString(replacementEntry, "#!/usr/bin/env node\n"); + const repairCommands: Array = []; + const repairService = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.28", + host: makeHost(replacementEntry), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(repairCommands, { failCommand: "loginctl" })), + provideHostRefs(dirs.home), + ); + + const error = yield* repairService.install.pipe(Effect.flip); + + assert.isTrue(isCommandError(error)); + assert.equal(yield* fs.readFileString(unitPath), previousUnit); + assert.isTrue( + repairCommands.some( + ({ command, args }) => + command === "systemctl" && args.join(" ") === "--user restart t3code.service", + ), + ); + }), + ); + + it.effect("keeps the unit when stopping it during uninstall fails", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const installCommands: Array = []; + const installedService = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(installCommands)), + provideHostRefs(dirs.home), + ); + yield* installedService.install; + + const uninstallCommands: Array = []; + const failingService = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost(dirs.stableEntry), + }).pipe( + Effect.provide( + makeRecordingRunnerLayer(uninstallCommands, { + failWhen: (command, args) => + command === "systemctl" && args.includes("disable") && args.includes("--now"), + }), + ), + provideHostRefs(dirs.home), + ); + + const error = yield* failingService.uninstall.pipe(Effect.flip); + + assert.isTrue(isCommandError(error)); + assert.isTrue( + yield* fs.exists(path.join(dirs.home, ".config", "systemd", "user", "t3code.service")), + ); + }), + ); + + it.effect("appends failed steps to the boot-service log", () => + Effect.gen(function* () { + const { dirs, fs, path } = yield* makeTestContext(); + const commands: Array = []; + const service = yield* BootService.make({ + baseDir: dirs.baseDir, + logsDir: dirs.logsDir, + cliVersion: "0.0.27", + host: makeHost("/usr/local/lib/node_modules/t3/dist/bin.mjs"), + }).pipe( + Effect.provide(makeRecordingRunnerLayer(commands, { failCommand: "systemctl" })), + provideHostRefs(dirs.home), + ); + + const error = yield* service.install.pipe(Effect.flip); + assert.isTrue(isCommandError(error)); + if (!isCommandError(error)) return; + assert.equal(error.exitCode, 1); + assert.equal(error.stderrLength, "systemctl exploded".length); + + const logPath = path.join(dirs.logsDir, "boot-service.log"); + assert.isTrue(yield* fs.exists(logPath)); + assert.include(yield* fs.readFileString(logPath), "exit code 1"); + }), + ); +}); diff --git a/apps/server/src/cloud/bootService.ts b/apps/server/src/cloud/bootService.ts new file mode 100644 index 00000000000..0662b367049 --- /dev/null +++ b/apps/server/src/cloud/bootService.ts @@ -0,0 +1,452 @@ +import * as Context from "effect/Context"; +import * as Config from "effect/Config"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; + +import { + HostProcessArguments, + HostProcessExecutablePath, + HostProcessPlatform, +} from "@t3tools/shared/hostProcess"; + +import * as ProcessRunner from "../processRunner.ts"; + +/** + * Installs T3 Code as a per-user boot service so a connected machine stays + * reachable through T3 Connect after the SSH session ends. Linux-only for + * now: systemd user unit + loginctl enable-linger. The service runs a pinned + * runtime installed under /runtime — never `npx t3`, whose cache is + * ephemeral and whose registry fetch at boot would make startup depend on + * the network. + */ + +const BOOT_SERVICE_NAME = "t3code"; +const BOOT_RUNTIME_DIR = "runtime"; + +const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`; +const PINNED_RUNTIME_INSTALL_TIMEOUT = Duration.minutes(10); + +const EPHEMERAL_CACHE_SEGMENTS = [ + "/_npx/", // npx + "\\_npx\\", + "/pnpm/dlx/", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) + "/.pnpm/dlx/", + "/.bun/install/cache/", // bunx +]; + +/** + * `npx t3` (and pnpm dlx / bunx) run out of ephemeral package-manager + * caches that can be evicted at any time — a boot service must never point + * there. Global installs, repo checkouts, and the pinned runtime below are + * all stable. + */ +export function isEphemeralCacheEntry(entryPath: string): boolean { + return EPHEMERAL_CACHE_SEGMENTS.some((segment) => entryPath.includes(segment)); +} + +/** + * systemd expands `%` specifiers in most directive values, including the + * `append:` file paths, which take the rest of the line literally and must + * NOT be quoted. + */ +export function escapeSystemdSpecifiers(value: string): string { + return value.replaceAll("%", "%%"); +} + +/** + * systemd word-splits ExecStart and Environment values and expands `%` + * specifiers, so paths with spaces or percents must be quoted and escaped. + */ +export function quoteSystemdValue(value: string): string { + const escaped = escapeSystemdSpecifiers(value); + return /[\s"'\\]/.test(escaped) + ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` + : escaped; +} + +export interface BootServicePlan { + /** Absolute path of the node binary running this CLI. */ + readonly nodePath: string; + /** Absolute path of the pinned t3 entry point the unit will run. */ + readonly t3EntryPath: string; + readonly baseDir: string; + readonly logPath: string; + readonly unitPath: string; +} + +/** + * Pure so it is testable byte-for-byte. systemd user units run with a + * minimal environment: every path must be absolute, and the service must + * not rely on PATH, nvm shims, or shell profiles. Failures land in + * `logPath` because `systemctl --user` failures are otherwise invisible. + */ +export function renderBootServiceUnit(plan: BootServicePlan): string { + // No After=network-online.target: it does not exist in the systemd *user* + // manager, so ordering on it is silently ignored. The server retries its + // relay connection, and Restart=always covers early-boot failures. + return [ + "[Unit]", + "Description=T3 Code server (T3 Connect)", + // Give up after 5 crashes in 5 minutes so a persistently broken install + // (deleted runtime, broken workspace) stops instead of restarting every + // 5s forever and growing the unrotated append log without bound. + "StartLimitIntervalSec=300", + "StartLimitBurst=5", + "", + "[Service]", + "Type=simple", + "WorkingDirectory=%h", + `Environment=T3CODE_HOME=${quoteSystemdValue(plan.baseDir)}`, + `ExecStart=${quoteSystemdValue(plan.nodePath)} ${quoteSystemdValue(plan.t3EntryPath)} serve`, + "Restart=always", + "RestartSec=5", + `StandardOutput=append:${escapeSystemdSpecifiers(plan.logPath)}`, + `StandardError=append:${escapeSystemdSpecifiers(plan.logPath)}`, + "", + "[Install]", + "WantedBy=default.target", + "", + ].join("\n"); +} + +export class BootServiceUnsupportedError extends Schema.TaggedErrorClass()( + "BootServiceUnsupportedError", + { platform: Schema.String }, +) { + override get message(): string { + return `Background setup currently supports Linux with systemd; this machine reports '${this.platform}'.`; + } +} + +export class BootServiceCommandError extends Schema.TaggedErrorClass()( + "BootServiceCommandError", + { + step: Schema.String, + exitCode: Schema.optional(Schema.Number), + stdoutLength: Schema.optional(Schema.Number), + stderrLength: Schema.optional(Schema.Number), + cause: Schema.optional(Schema.Defect()), + }, +) { + override get message(): string { + return this.exitCode === undefined + ? `Background setup failed while ${this.step}.` + : `Background setup failed while ${this.step} (exit code ${this.exitCode}).`; + } +} + +export class BootServiceInstallError extends Schema.TaggedErrorClass()( + "BootServiceInstallError", + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Could not set up the T3 Code background service."; + } +} + +export type BootServiceError = + | BootServiceUnsupportedError + | BootServiceCommandError + | BootServiceInstallError; + +export interface BootServiceStatus { + readonly supported: boolean; + readonly installed: boolean; + /** False when the installed unit no longer matches what install would write. */ + readonly current: boolean; + readonly unitPath: string; + readonly logPath: string; +} + +export class BootService extends Context.Service< + BootService, + { + /** Installs the pinned runtime + unit, enables linger, starts the service. */ + readonly install: Effect.Effect; + /** + * Stops and removes the unit; leaves the pinned runtime for reuse. + * Returns whether a unit was actually removed. + */ + readonly uninstall: Effect.Effect; + readonly status: Effect.Effect; + } +>()("t3/cloud/bootService") {} + +export interface BootServiceHost { + readonly execPath: string; + readonly cliEntryPath: string; +} + +export const make = Effect.fn("cloud.boot_service.make")(function* (input: { + readonly baseDir: string; + readonly logsDir: string; + readonly cliVersion: string; + readonly host?: BootServiceHost; +}) { + const hostExecPath = yield* HostProcessExecutablePath; + const hostArguments = yield* HostProcessArguments; + const host = input.host ?? { + execPath: hostExecPath, + // When running the packed CLI this is dist/bin.mjs; when stable (global + // install, repo checkout) the boot service runs this same artifact. + cliEntryPath: hostArguments[1] ?? "", + }; + const platform = yield* HostProcessPlatform; + const homeDir = yield* Config.string("HOME").pipe(Config.withDefault("")); + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const runner = yield* ProcessRunner.ProcessRunner; + + const unitDir = path.join(homeDir, ".config", "systemd", "user"); + const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE); + const logPath = path.join(input.logsDir, "boot-service.log"); + const runtimeVersionDir = path.join( + input.baseDir, + BOOT_RUNTIME_DIR, + "versions", + input.cliVersion, + ); + const runtimeEntryPath = path.join(runtimeVersionDir, "node_modules", "t3", "dist", "bin.mjs"); + const runtimeSentinelPath = path.join(runtimeVersionDir, ".install-complete"); + + const requireSystemdLinux = Effect.gen(function* () { + if (platform !== "linux" || homeDir === "") { + return yield* new BootServiceUnsupportedError({ platform }); + } + }); + + const runStep = Effect.fn("cloud.boot_service.run_step")(function* ( + step: string, + command: string, + args: ReadonlyArray, + options?: { readonly timeout?: Duration.Input }, + ) { + return yield* runner.run({ command, args, timeout: options?.timeout }).pipe( + Effect.mapError((cause) => new BootServiceCommandError({ step, cause })), + Effect.filterOrFail( + (result) => result.code === 0, + (result) => + new BootServiceCommandError({ + step, + exitCode: Number(result.code), + stdoutLength: result.stdout.length, + stderrLength: result.stderr.length, + }), + ), + Effect.tapError((error) => + DateTime.now.pipe( + Effect.flatMap((now) => + fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, { + flag: "a", + }), + ), + Effect.ignore, + ), + ), + ); + }); + + /** + * Ensures plannedEntryPath exists before the unit points at it. A stable + * install (global bin, repo checkout) is used as-is; an ephemeral cache + * entry is replaced by `npm install --prefix`-ing the exact running + * version into /runtime/versions/. A real install (not a copy + * of bin.mjs) because t3 ships native deps like node-pty. + */ + const ensurePinnedRuntime = Effect.gen(function* () { + if (!isEphemeralCacheEntry(host.cliEntryPath)) { + return; + } + // The sentinel is written only after npm exits 0. Checking the entry + // file alone is not enough: npm extracts files before running native + // builds (node-pty), so a killed install leaves a plausible-looking but + // broken tree behind. + const alreadyPinned = yield* Effect.all([ + fs.exists(runtimeSentinelPath), + fs.exists(runtimeEntryPath), + ]).pipe( + Effect.map(([sentinelExists, entryExists]) => sentinelExists && entryExists), + Effect.mapError((cause) => new BootServiceInstallError({ cause })), + ); + if (alreadyPinned) { + return; + } + yield* fs.remove(runtimeVersionDir, { recursive: true, force: true }).pipe( + Effect.andThen(fs.makeDirectory(runtimeVersionDir, { recursive: true })), + Effect.mapError((cause) => new BootServiceInstallError({ cause })), + ); + yield* runStep( + "installing the pinned t3 runtime (this can take a few minutes)", + "npm", + [ + "install", + "--prefix", + runtimeVersionDir, + "--no-fund", + "--no-audit", + `t3@${input.cliVersion}`, + ], + // Native deps (node-pty) can compile from source on slow boxes; the + // ProcessRunner default of 60s would kill a healthy install. + { timeout: PINNED_RUNTIME_INSTALL_TIMEOUT }, + ).pipe( + Effect.tapError(() => + fs.remove(runtimeVersionDir, { recursive: true, force: true }).pipe(Effect.ignore), + ), + ); + yield* fs + .writeFileString(runtimeSentinelPath, `${input.cliVersion}\n`) + .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); + }); + + // Where the unit will point: derivable without touching the network, so + // status can compare units purely; install materializes it first. + const plannedEntryPath = isEphemeralCacheEntry(host.cliEntryPath) + ? runtimeEntryPath + : host.cliEntryPath; + const plan: BootServicePlan = { + nodePath: host.execPath, + t3EntryPath: plannedEntryPath, + baseDir: input.baseDir, + logPath, + unitPath, + }; + + const install: BootService["Service"]["install"] = Effect.gen(function* () { + yield* requireSystemdLinux; + yield* fs + .makeDirectory(input.logsDir, { recursive: true }) + .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); + + yield* ensurePinnedRuntime; + + const previousUnit = yield* fs.exists(unitPath).pipe( + Effect.flatMap((exists) => + exists + ? fs.readFileString(unitPath).pipe(Effect.map(Option.some)) + : Effect.succeed(Option.none()), + ), + Effect.mapError((cause) => new BootServiceInstallError({ cause })), + ); + + yield* fs.makeDirectory(unitDir, { recursive: true }).pipe( + Effect.andThen(fs.writeFileString(unitPath, renderBootServiceUnit(plan))), + Effect.mapError((cause) => new BootServiceInstallError({ cause })), + ); + + // If any activation step fails, remove the unit again: a leftover file + // would make the next `t3 connect` report the service as already set up + // even though it was never enabled or lingered. + yield* Effect.gen(function* () { + yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]); + yield* runStep("enabling the service", "systemctl", [ + "--user", + "enable", + BOOT_SERVICE_UNIT_FILE, + ]); + // restart rather than enable --now: --now does not replace an already + // running process, so repairing a stale unit would leave the old + // server running until reboot. restart also starts a stopped service. + yield* runStep("starting the service", "systemctl", [ + "--user", + "restart", + BOOT_SERVICE_UNIT_FILE, + ]); + // Linger keeps the user manager (and this service) running without an + // open session — the whole point on a box reached over SSH. No + // username argument: loginctl defaults to the calling user, which is + // always right, while $USER can be stale (su without -l) or unset. + yield* runStep("enabling lingering for this user", "loginctl", ["enable-linger"]); + }).pipe(Effect.tapError(() => rollbackFailedInstall(previousUnit))); + + return plan; + }).pipe(Effect.withSpan("cloud.boot_service.install")); + + // If activation fails partway (e.g. enable succeeds but restart/linger + // fails), leave nothing behind: disable removes the enable symlink, remove + // deletes the file, daemon-reload clears the stale definition — otherwise a + // dangling wants/ symlink logs "Failed to load unit" at every boot and the + // next connect misreports the state. + const rollbackFailedInstall = Effect.fn("cloud.boot_service.rollback_failed_install")(function* ( + previousUnit: Option.Option, + ) { + if (Option.isSome(previousUnit)) { + yield* fs.writeFileString(unitPath, previousUnit.value).pipe(Effect.ignore); + } else { + yield* runStep("cleaning up the service", "systemctl", [ + "--user", + "disable", + "--now", + BOOT_SERVICE_UNIT_FILE, + ]).pipe(Effect.ignore); + yield* fs.remove(unitPath).pipe(Effect.ignore); + } + yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]).pipe( + Effect.ignore, + ); + if (Option.isSome(previousUnit)) { + yield* runStep("restoring the previous service", "systemctl", [ + "--user", + "restart", + BOOT_SERVICE_UNIT_FILE, + ]).pipe(Effect.ignore); + } + }); + + const uninstall: BootService["Service"]["uninstall"] = Effect.gen(function* () { + yield* requireSystemdLinux; + const exists = yield* fs + .exists(unitPath) + .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); + if (!exists) { + return false; + } + yield* runStep("stopping the service", "systemctl", [ + "--user", + "disable", + "--now", + BOOT_SERVICE_UNIT_FILE, + ]); + yield* fs + .remove(unitPath) + .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); + yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]); + return true; + }).pipe(Effect.withSpan("cloud.boot_service.uninstall")); + + const status: BootService["Service"]["status"] = Effect.gen(function* () { + if (platform !== "linux" || homeDir === "") { + return { supported: false, installed: false, current: false, unitPath, logPath }; + } + const unitExists = yield* fs.exists(unitPath); + if (!unitExists) { + return { supported: true, installed: false, current: false, unitPath, logPath }; + } + const unit = yield* fs.readFileString(unitPath); + // A unit is current only if it matches what install would write now (an + // older CLI wrote a different runtime/node path) AND the entry point it + // references still exists (a pinned runtime under ~/.t3 can be deleted to + // reclaim space). Either mismatch makes connect offer a repair. + const entryExists = yield* fs.exists(plannedEntryPath); + const current = unit === renderBootServiceUnit(plan) && entryExists; + return { supported: true, installed: true, current, unitPath, logPath }; + }).pipe( + Effect.mapError((cause) => new BootServiceInstallError({ cause })), + Effect.withSpan("cloud.boot_service.status"), + ); + + return BootService.of({ install, uninstall, status }); +}); + +export const layer = (input: { + readonly baseDir: string; + readonly logsDir: string; + readonly cliVersion: string; + readonly host?: BootServiceHost; +}) => Layer.effect(BootService, make(input)); diff --git a/apps/server/src/cloud/cliAuthHtml.test.ts b/apps/server/src/cloud/cliAuthHtml.test.ts new file mode 100644 index 00000000000..a49eaffeda7 --- /dev/null +++ b/apps/server/src/cloud/cliAuthHtml.test.ts @@ -0,0 +1,13 @@ +import { expect, it } from "@effect/vitest"; + +import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts"; + +it("renders the branded loopback authorization completion page", () => { + const html = renderLoopbackAuthorizationCompleteHtml(); + + expect(html).toContain("T3 Code"); + expect(html).not.toContain("Secure terminal handoff"); + expect(html).toContain("You're connected"); + expect(html).toContain("return to the terminal"); + expect(html).toContain('name="viewport"'); +}); diff --git a/apps/server/src/cloud/cliAuthHtml.ts b/apps/server/src/cloud/cliAuthHtml.ts new file mode 100644 index 00000000000..3c4141fba44 --- /dev/null +++ b/apps/server/src/cloud/cliAuthHtml.ts @@ -0,0 +1,140 @@ +export function renderLoopbackAuthorizationCompleteHtml(): string { + return ` + + + + + + T3 Connect authorization complete + + + +
+
+
+

T3 Code

+
+
+
+ +

Browser authorization complete

+

You're connected

+

The authorization code was delivered securely to your waiting terminal.

+ +
+
+ +`; +} diff --git a/apps/server/src/cloud/http.test.ts b/apps/server/src/cloud/http.test.ts index bab9cdd27f3..96abf2e4b4a 100644 --- a/apps/server/src/cloud/http.test.ts +++ b/apps/server/src/cloud/http.test.ts @@ -200,6 +200,7 @@ describe("reconcileDesiredCloudLink", () => { get: unusedSecretStoreOperation(), getExisting: Effect.succeed(Option.none()), hasCredential: unusedSecretStoreOperation(), + store: () => unusedSecretStoreOperation(), clear: unusedSecretStoreOperation(), }), ), diff --git a/apps/server/src/cloud/publicConfig.test.ts b/apps/server/src/cloud/publicConfig.test.ts index c46e2671a46..96a8a1b8b8a 100644 --- a/apps/server/src/cloud/publicConfig.test.ts +++ b/apps/server/src/cloud/publicConfig.test.ts @@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"; import * as Result from "effect/Result"; import { + hostedAppUrlConfig, makeCloudCliOAuthConfig, makeRelayUrlConfig, resolveRelayClientTracingConfig, @@ -47,6 +48,40 @@ it.effect("rejects an injected relay URL with a non-origin path", () => makeRelayUrlConfig("https://embedded.example.test/path").pipe(provideEnv({}), Effect.flip), ); +it.effect("normalizes the hosted app URL to an absolute origin", () => + Effect.gen(function* () { + assert.equal( + yield* hostedAppUrlConfig.pipe( + provideEnv({ T3CODE_HOSTED_APP_URL: "https://nightly.app.t3.codes" }), + ), + "https://nightly.app.t3.codes", + ); + assert.equal( + yield* hostedAppUrlConfig.pipe( + provideEnv({ T3CODE_HOSTED_APP_URL: "http://localhost:5733" }), + ), + "http://localhost:5733", + ); + }), +); + +it.effect("rejects malformed or insecure hosted app URLs", () => + Effect.gen(function* () { + for (const value of [ + "app.t3.codes", + "http://app.t3.codes", + "https://app.t3.codes/nested", + "https://app.t3.codes?alias=true", + ]) { + const result = yield* hostedAppUrlConfig.pipe( + provideEnv({ T3CODE_HOSTED_APP_URL: value }), + Effect.result, + ); + assert.isTrue(Result.isFailure(result), value); + } + }), +); + it.effect("derives direct Clerk OAuth endpoints from statically injected public config", () => Effect.gen(function* () { const config = yield* makeCloudCliOAuthConfig({ diff --git a/apps/server/src/cloud/publicConfig.ts b/apps/server/src/cloud/publicConfig.ts index 176b31d7566..cb07d19721b 100644 --- a/apps/server/src/cloud/publicConfig.ts +++ b/apps/server/src/cloud/publicConfig.ts @@ -1,3 +1,4 @@ +import { CONNECT_OAUTH_SCOPES, DEFAULT_HOSTED_APP_URL } from "@t3tools/shared/connectAuth"; import { clerkFrontendApiUrlFromPublishableKey } from "@t3tools/shared/relayAuth"; import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl"; import * as Config from "effect/Config"; @@ -15,7 +16,7 @@ declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: string | undefi declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: string | undefined; const CLOUD_CLI_OAUTH_REDIRECT_URI = "http://127.0.0.1:34338/callback"; -const CLOUD_CLI_OAUTH_SCOPES = ["openid", "profile", "email"] as const; +const CLOUD_CLI_OAUTH_SCOPES = CONNECT_OAUTH_SCOPES; function validateRelayUrl(value: string) { const relayUrl = normalizeSecureRelayUrl(value); @@ -100,6 +101,44 @@ export function makeRelayUrlConfig(fallback = buildTimeRelayUrl) { export const relayUrlConfig = makeRelayUrlConfig(); +/** + * Hosted app origin used for out-of-band OAuth on headless + * machines. Overridable so staging/nightly builds can point their CLIs at a + * matching hosted deployment. + */ +export const hostedAppUrlConfig = makePublicValueConfig( + "T3CODE_HOSTED_APP_URL", + DEFAULT_HOSTED_APP_URL, +).pipe(Config.mapOrFail(validateHostedAppUrl)); + +function validateHostedAppUrl(value: string) { + try { + const url = new URL(value); + const isLoopbackHttp = + url.protocol === "http:" && + (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]"); + if ( + (url.protocol !== "https:" && !isLoopbackHttp) || + url.pathname !== "/" || + url.search !== "" || + url.hash !== "" + ) { + throw new Error("invalid hosted app origin"); + } + return Effect.succeed(url.origin); + } catch { + return Effect.fail( + new Config.ConfigError( + new Schema.SchemaError( + new SchemaIssue.InvalidValue(Option.some(value), { + message: "Hosted app URL must be an absolute HTTPS origin (or HTTP loopback origin).", + }), + ), + ), + ); + } +} + function makePublicValueConfig(name: string, fallback: string) { const runtimeConfig = Config.nonEmptyString(name); return (fallback ? runtimeConfig.pipe(Config.withDefault(fallback)) : runtimeConfig).pipe( diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index ba1131ee259..d468838d5d4 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -123,15 +123,11 @@ export interface RunMigrationsOptions { export const runMigrations = Effect.fn("runMigrations")(function* ({ toMigrationInclusive, }: RunMigrationsOptions = {}) { - yield* Effect.log( - toMigrationInclusive === undefined - ? "Running all migrations..." - : `Running migrations 1 through ${toMigrationInclusive}...`, - ); const executedMigrations = yield* run({ loader: makeMigrationLoader(toMigrationInclusive) }); - yield* Effect.log("Migrations ran successfully").pipe( - Effect.annotateLogs({ migrations: executedMigrations.map(([id, name]) => `${id}_${name}`) }), - ); + const migrations = executedMigrations.map(([id, name]) => `${id}_${name}`); + yield* migrations.length === 0 + ? Effect.logDebug("Database schema is current") + : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations })); return executedMigrations; }); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 0c632d8486c..0e6db87b109 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -322,7 +322,10 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( Layer.provideMerge(ServerSecretStore.layer), Layer.provideMerge( Layer.mergeAll( - CloudCliTokenManager.layer.pipe(Layer.provide(ServerSecretStore.layer)), + CloudCliTokenManager.layer.pipe( + Layer.provide(ServerSecretStore.layer), + Layer.provide(ExternalLauncher.layer), + ), CloudManagedEndpointRuntimeLive, ), ), diff --git a/apps/web/src/cloud/connectCliAuth.test.ts b/apps/web/src/cloud/connectCliAuth.test.ts new file mode 100644 index 00000000000..61d854eb6ab --- /dev/null +++ b/apps/web/src/cloud/connectCliAuth.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; + +import { + buildConnectCliClerkAuthorizeUrl, + hasConnectCliAuthConfig, + readConnectCliCallbackResult, +} from "./connectCliAuth"; + +// Any pk_test_* key decodes to .clerk.accounts.dev. +const TEST_PUBLISHABLE_KEY = `pk_test_${btoa("witty-mole-42.clerk.accounts.dev$")}`; + +describe("connectCliAuth", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("requires both the publishable key and the CLI OAuth client id", () => { + vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", TEST_PUBLISHABLE_KEY); + vi.stubEnv("VITE_CLERK_JWT_TEMPLATE", "t3-relay"); + vi.stubEnv("VITE_T3CODE_RELAY_URL", "https://relay.example.com"); + expect(hasConnectCliAuthConfig()).toBe(false); + + vi.stubEnv("VITE_CLERK_CLI_OAUTH_CLIENT_ID", "oauthapp_123"); + expect(hasConnectCliAuthConfig()).toBe(true); + }); + + it("builds the Clerk authorize URL with the configured hosted origin's callback", () => { + vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", TEST_PUBLISHABLE_KEY); + vi.stubEnv("VITE_CLERK_CLI_OAUTH_CLIENT_ID", "oauthapp_123"); + vi.stubEnv("VITE_HOSTED_APP_URL", "https://nightly.app.t3.codes"); + + const authorizeUrl = buildConnectCliClerkAuthorizeUrl({ + state: "state-1", + challenge: "challenge-1", + }); + expect(authorizeUrl).not.toBeNull(); + + const url = new URL(authorizeUrl!); + expect(url.hostname).toBe("witty-mole-42.clerk.accounts.dev"); + expect(url.pathname).toBe("/oauth/authorize"); + expect(url.searchParams.get("redirect_uri")).toBe( + "https://nightly.app.t3.codes/connect/callback", + ); + expect(url.searchParams.get("state")).toBe("state-1"); + expect(url.searchParams.get("code_challenge")).toBe("challenge-1"); + expect(url.searchParams.get("code_challenge_method")).toBe("S256"); + }); + + it("returns null when the CLI OAuth client id is not configured", () => { + vi.stubEnv("VITE_CLERK_PUBLISHABLE_KEY", TEST_PUBLISHABLE_KEY); + expect( + buildConnectCliClerkAuthorizeUrl({ state: "state-1", challenge: "challenge-1" }), + ).toBeNull(); + }); + + it("reads the code and state Clerk echoes back to the callback", () => { + expect( + readConnectCliCallbackResult( + new URL("https://app.t3.codes/connect/callback?code=abc&state=state-1"), + ), + ).toEqual({ code: "abc", state: "state-1" }); + expect( + readConnectCliCallbackResult(new URL("https://app.t3.codes/connect/callback?code=abc")), + ).toBeNull(); + expect( + readConnectCliCallbackResult(new URL("https://app.t3.codes/connect/callback?state=s")), + ).toBeNull(); + }); +}); diff --git a/apps/web/src/cloud/connectCliAuth.ts b/apps/web/src/cloud/connectCliAuth.ts new file mode 100644 index 00000000000..849319dcebe --- /dev/null +++ b/apps/web/src/cloud/connectCliAuth.ts @@ -0,0 +1,91 @@ +import { + buildConnectClerkAuthorizeUrl, + connectCallbackUrl, + CONNECT_OAUTH_SCOPES, + type ConnectAuthorizeRequest, +} from "@t3tools/shared/connectAuth"; +import { clerkFrontendApiUrlFromPublishableKey } from "@t3tools/shared/relayAuth"; + +import { configuredHostedAppUrl, isHostedStaticApp } from "../hostedPairing"; +import { hasCloudPublicConfig, resolveCloudPublicConfig, trimNonEmpty } from "./publicConfig"; + +const CONNECT_CLI_AUTH_STATE_STORAGE_KEY = "t3code-connect-cli-auth-state"; + +export function resolveConnectCliOAuthClientId(): string | null { + return trimNonEmpty(import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID as string | undefined); +} + +export function hasConnectCliAuthConfig(): boolean { + return Boolean( + resolveCloudPublicConfig().clerkPublishableKey && resolveConnectCliOAuthClientId(), + ); +} + +/** + * Gate for the /connect routes: the CLI handshake only exists on the hosted + * deployment (the same bundle ships inside local instances) and needs the + * Clerk CLI OAuth client configured at build time. + */ +export function connectCliAuthRoutesEnabled(): boolean { + return isHostedStaticApp() && hasCloudPublicConfig() && hasConnectCliAuthConfig(); +} + +/** + * Builds the Clerk authorize URL for a CLI-initiated connect request. The + * state is mirrored into sessionStorage so the callback page can verify the + * response matches a request this browser actually started. + */ +export function buildConnectCliClerkAuthorizeUrl(request: ConnectAuthorizeRequest): string | null { + const { clerkPublishableKey } = resolveCloudPublicConfig(); + const clientId = resolveConnectCliOAuthClientId(); + if (!clerkPublishableKey || !clientId) { + return null; + } + return buildConnectClerkAuthorizeUrl({ + authorizationEndpoint: `${clerkFrontendApiUrlFromPublishableKey(clerkPublishableKey)}/oauth/authorize`, + clientId, + redirectUri: connectCallbackUrl(configuredHostedAppUrl()), + scopes: CONNECT_OAUTH_SCOPES, + state: request.state, + challenge: request.challenge, + }); +} + +export function rememberConnectCliAuthState(state: string): void { + try { + window.sessionStorage.setItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY, state); + } catch { + // Session storage can be unavailable (e.g. blocked). The callback page + // then falls back to trusting the state Clerk echoed back. + } +} + +/** + * Read-only on purpose: this runs during render, where a removal would be + * consumed by React's double-invoked/discarded renders (StrictMode) and + * silently disable the state check. The value is not a secret and is + * overwritten by the next /connect visit. + */ +export function readConnectCliAuthState(): string | null { + try { + return window.sessionStorage.getItem(CONNECT_CLI_AUTH_STATE_STORAGE_KEY); + } catch { + return null; + } +} + +export interface ConnectCliCallbackResult { + readonly code: string; + readonly state: string; +} + +export function readConnectCliCallbackResult( + url: URL = new URL(window.location.href), +): ConnectCliCallbackResult | null { + const code = url.searchParams.get("code")?.trim() ?? ""; + const state = url.searchParams.get("state")?.trim() ?? ""; + if (!code || !state) { + return null; + } + return { code, state }; +} diff --git a/apps/web/src/cloud/publicConfig.ts b/apps/web/src/cloud/publicConfig.ts index d9d0e5f44cb..2caf4f52f36 100644 --- a/apps/web/src/cloud/publicConfig.ts +++ b/apps/web/src/cloud/publicConfig.ts @@ -24,7 +24,7 @@ export interface CloudPublicConfig { }; } -function trimNonEmpty(value: string | undefined): string | null { +export function trimNonEmpty(value: string | undefined): string | null { return value?.trim() || null; } diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx index 963a8c2a951..d3d86186c03 100644 --- a/apps/web/src/components/SidebarStageBackdrop.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -38,11 +38,15 @@ export function SidebarStageBackdrop({ variant }: { variant: SidebarStageBackdro aria-hidden className="sidebar-stage-backdrop pointer-events-none absolute inset-x-0 top-0 z-0 h-20 select-none overflow-hidden" > - {variant === "nightly" ? : } + ); } +export function StageBackdropArt({ variant }: { variant: SidebarStageBackdropVariant }) { + return variant === "nightly" ? : ; +} + const NIGHTLY_STARS: ReadonlyArray<{ cx: number; cy: number; diff --git a/apps/web/src/components/auth/AuthSurfaceShell.tsx b/apps/web/src/components/auth/AuthSurfaceShell.tsx new file mode 100644 index 00000000000..1c45c98a9e4 --- /dev/null +++ b/apps/web/src/components/auth/AuthSurfaceShell.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from "react"; + +import { APP_DISPLAY_NAME, APP_STAGE_LABEL } from "../../branding"; +import { resolveSidebarStageBackdropVariant, StageBackdropArt } from "../SidebarStageBackdrop"; + +/** + * Full-screen card for standalone auth pages, mirroring the pairing surface's + * treatment. Used by the CLI-connect authorize and callback surfaces. + */ +export function AuthSurfaceShell({ children }: { readonly children: ReactNode }) { + const stageVariant = resolveSidebarStageBackdropVariant(APP_STAGE_LABEL); + + return ( +
+
+
+
+
+ +
+
+ {stageVariant ? ( +
+ +
+ ) : ( +
+ )} +
+
+

+ {APP_DISPLAY_NAME} +

+
+
+ +
{children}
+
+
+ ); +} diff --git a/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx b/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx new file mode 100644 index 00000000000..40ddc4c3dd9 --- /dev/null +++ b/apps/web/src/components/cloud/ConnectCliAuthSurface.tsx @@ -0,0 +1,188 @@ +import { useAuth, useClerk, useUser } from "@clerk/react"; +import { encodeConnectAuthCode, readConnectAuthorizeRequest } from "@t3tools/shared/connectAuth"; +import { useEffect, useRef, useState } from "react"; + +import { + buildConnectCliClerkAuthorizeUrl, + readConnectCliAuthState, + readConnectCliCallbackResult, + rememberConnectCliAuthState, +} from "../../cloud/connectCliAuth"; +import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; +import { AuthSurfaceShell } from "../auth/AuthSurfaceShell"; +import { Button } from "../ui/button"; + +function ConnectCliAuthMessage({ + eyebrow, + title, + description, +}: { + readonly eyebrow?: string; + readonly title: string; + readonly description: string; +}) { + return ( + <> + {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

{title}

+

{description}

+ + ); +} + +const invalidLinkMessage = { + eyebrow: "Authorization request", + title: "This connect link is incomplete", + description: + "The link is missing its authorization request. Re-run `t3 connect` in your terminal and open the freshly printed URL.", +} as const; + +/** + * /connect: the URL a headless CLI prints. Waits for a Clerk session, then + * forwards the CLI's PKCE request to Clerk's authorize endpoint. + */ +export function ConnectCliAuthorizeSurface() { + const [request] = useState(() => readConnectAuthorizeRequest(new URL(window.location.href))); + const clerk = useClerk(); + const { isLoaded, isSignedIn } = useAuth(); + const signInOpened = useRef(false); + const redirecting = useRef(false); + + useEffect(() => { + if (!request || !isLoaded || redirecting.current) { + return; + } + if (!isSignedIn) { + if (!signInOpened.current) { + signInOpened.current = true; + clerk.openSignIn({ forceRedirectUrl: window.location.href }); + } + return; + } + const authorizeUrl = buildConnectCliClerkAuthorizeUrl(request); + if (!authorizeUrl) { + return; + } + redirecting.current = true; + rememberConnectCliAuthState(request.state); + window.location.assign(authorizeUrl); + }, [clerk, isLoaded, isSignedIn, request]); + + if (!request) { + return ( + + + + ); + } + + return ( + + + {isLoaded && !isSignedIn ? ( +
+ +
+ ) : null} +
+ ); +} + +/** + * /connect/callback: Clerk's redirect target. Shows the one-time code the + * user enters in the waiting terminal. + */ +export function ConnectCliCallbackSurface() { + const [result] = useState(readConnectCliCallbackResult); + const [expectedState] = useState(readConnectCliAuthState); + const { user } = useUser(); + const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "authentication code" }); + + if (!result) { + return ( + + + + ); + } + + // Fail closed: the legitimate callback always lands in the same browser + // that visited /connect (which recorded the state), so a missing or + // mismatched state means this page was reached some other way — the CSRF + // shape the state parameter exists to stop. Refuse to display a code. + if (expectedState === null || expectedState !== result.state) { + return ( + + + + ); + } + + const accountLabel = user?.primaryEmailAddress?.emailAddress ?? user?.username ?? null; + const authCode = encodeConnectAuthCode(result); + + return ( + + + +
+
+ + One-time authorization code + + expires shortly +
+ + {authCode} + +
+ +
+ +
+ +

+ Only enter this code in a terminal session you started yourself. Anyone holding it can link + their machine to your T3 Connect account while it is valid. +

+
+ ); +} diff --git a/apps/web/src/hostedPairing.ts b/apps/web/src/hostedPairing.ts index 2caf844af9b..6a9815b4b33 100644 --- a/apps/web/src/hostedPairing.ts +++ b/apps/web/src/hostedPairing.ts @@ -1,6 +1,6 @@ -import { getPairingTokenFromUrl, setPairingTokenOnUrl } from "./pairingUrl"; +import { DEFAULT_HOSTED_APP_URL } from "@t3tools/shared/connectAuth"; -const DEFAULT_HOSTED_APP_URL = "https://app.t3.codes"; +import { getPairingTokenFromUrl, setPairingTokenOnUrl } from "./pairingUrl"; export interface HostedPairingRequest { readonly host: string; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 3a9140e278c..b524fe018e9 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SettingsRouteImport } from './routes/settings' import { Route as PairRouteImport } from './routes/pair' +import { Route as ConnectRouteImport } from './routes/connect' import { Route as ChatRouteImport } from './routes/_chat' import { Route as ChatIndexRouteImport } from './routes/_chat.index' import { Route as SettingsSourceControlRouteImport } from './routes/settings.source-control' @@ -20,6 +21,7 @@ import { Route as SettingsGeneralRouteImport } from './routes/settings.general' import { Route as SettingsDiagnosticsRouteImport } from './routes/settings.diagnostics' import { Route as SettingsConnectionsRouteImport } from './routes/settings.connections' import { Route as SettingsArchivedRouteImport } from './routes/settings.archived' +import { Route as ConnectCallbackRouteImport } from './routes/connect_.callback' import { Route as ChatDraftDraftIdRouteImport } from './routes/_chat.draft.$draftId' import { Route as ChatEnvironmentIdThreadIdRouteImport } from './routes/_chat.$environmentId.$threadId' @@ -33,6 +35,11 @@ const PairRoute = PairRouteImport.update({ path: '/pair', getParentRoute: () => rootRouteImport, } as any) +const ConnectRoute = ConnectRouteImport.update({ + id: '/connect', + path: '/connect', + getParentRoute: () => rootRouteImport, +} as any) const ChatRoute = ChatRouteImport.update({ id: '/_chat', getParentRoute: () => rootRouteImport, @@ -77,6 +84,11 @@ const SettingsArchivedRoute = SettingsArchivedRouteImport.update({ path: '/archived', getParentRoute: () => SettingsRoute, } as any) +const ConnectCallbackRoute = ConnectCallbackRouteImport.update({ + id: '/connect_/callback', + path: '/connect/callback', + getParentRoute: () => rootRouteImport, +} as any) const ChatDraftDraftIdRoute = ChatDraftDraftIdRouteImport.update({ id: '/draft/$draftId', path: '/draft/$draftId', @@ -91,8 +103,10 @@ const ChatEnvironmentIdThreadIdRoute = export interface FileRoutesByFullPath { '/': typeof ChatIndexRoute + '/connect': typeof ConnectRoute '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/connect/callback': typeof ConnectCallbackRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -104,8 +118,10 @@ export interface FileRoutesByFullPath { '/draft/$draftId': typeof ChatDraftDraftIdRoute } export interface FileRoutesByTo { + '/connect': typeof ConnectRoute '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/connect/callback': typeof ConnectCallbackRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -120,8 +136,10 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/_chat': typeof ChatRouteWithChildren + '/connect': typeof ConnectRoute '/pair': typeof PairRoute '/settings': typeof SettingsRouteWithChildren + '/connect_/callback': typeof ConnectCallbackRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute @@ -137,8 +155,10 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/connect' | '/pair' | '/settings' + | '/connect/callback' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -150,8 +170,10 @@ export interface FileRouteTypes { | '/draft/$draftId' fileRoutesByTo: FileRoutesByTo to: + | '/connect' | '/pair' | '/settings' + | '/connect/callback' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -165,8 +187,10 @@ export interface FileRouteTypes { id: | '__root__' | '/_chat' + | '/connect' | '/pair' | '/settings' + | '/connect_/callback' | '/settings/archived' | '/settings/connections' | '/settings/diagnostics' @@ -181,8 +205,10 @@ export interface FileRouteTypes { } export interface RootRouteChildren { ChatRoute: typeof ChatRouteWithChildren + ConnectRoute: typeof ConnectRoute PairRoute: typeof PairRoute SettingsRoute: typeof SettingsRouteWithChildren + ConnectCallbackRoute: typeof ConnectCallbackRoute } declare module '@tanstack/react-router' { @@ -201,6 +227,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PairRouteImport parentRoute: typeof rootRouteImport } + '/connect': { + id: '/connect' + path: '/connect' + fullPath: '/connect' + preLoaderRoute: typeof ConnectRouteImport + parentRoute: typeof rootRouteImport + } '/_chat': { id: '/_chat' path: '' @@ -264,6 +297,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsArchivedRouteImport parentRoute: typeof SettingsRoute } + '/connect_/callback': { + id: '/connect_/callback' + path: '/connect/callback' + fullPath: '/connect/callback' + preLoaderRoute: typeof ConnectCallbackRouteImport + parentRoute: typeof rootRouteImport + } '/_chat/draft/$draftId': { id: '/_chat/draft/$draftId' path: '/draft/$draftId' @@ -321,8 +361,10 @@ const SettingsRouteWithChildren = SettingsRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { ChatRoute: ChatRouteWithChildren, + ConnectRoute: ConnectRoute, PairRoute: PairRoute, SettingsRoute: SettingsRouteWithChildren, + ConnectCallbackRoute: ConnectCallbackRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 69739ce4570..ff6bc5b3952 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -97,7 +97,7 @@ function RootRouteView() { }; }, [pathname]); - if (pathname === "/pair") { + if (pathname === "/pair" || pathname === "/connect" || pathname.startsWith("/connect/")) { return ( <> diff --git a/apps/web/src/routes/connect.tsx b/apps/web/src/routes/connect.tsx new file mode 100644 index 00000000000..30750a977ea --- /dev/null +++ b/apps/web/src/routes/connect.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +import { connectCliAuthRoutesEnabled } from "../cloud/connectCliAuth"; +import { ConnectCliAuthorizeSurface } from "../components/cloud/ConnectCliAuthSurface"; + +export const Route = createFileRoute("/connect")({ + beforeLoad: () => { + if (!connectCliAuthRoutesEnabled()) { + throw redirect({ to: "/", replace: true }); + } + }, + component: ConnectCliAuthorizeSurface, +}); diff --git a/apps/web/src/routes/connect_.callback.tsx b/apps/web/src/routes/connect_.callback.tsx new file mode 100644 index 00000000000..a5beee0b9d3 --- /dev/null +++ b/apps/web/src/routes/connect_.callback.tsx @@ -0,0 +1,13 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +import { connectCliAuthRoutesEnabled } from "../cloud/connectCliAuth"; +import { ConnectCliCallbackSurface } from "../components/cloud/ConnectCliAuthSurface"; + +export const Route = createFileRoute("/connect_/callback")({ + beforeLoad: () => { + if (!connectCliAuthRoutesEnabled()) { + throw redirect({ to: "/", replace: true }); + } + }, + component: ConnectCliCallbackSurface, +}); diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index d8a6d71b49a..ac1da93f5c8 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -9,6 +9,7 @@ interface ImportMetaEnv { readonly VITE_HOSTED_APP_CHANNEL: string; readonly VITE_CLERK_PUBLISHABLE_KEY: string; readonly VITE_CLERK_JWT_TEMPLATE: string; + readonly VITE_CLERK_CLI_OAUTH_CLIENT_ID: string; readonly VITE_RELAY_OTLP_TRACES_URL: string; readonly VITE_RELAY_OTLP_TRACES_DATASET: string; readonly VITE_RELAY_OTLP_TRACES_TOKEN: string; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index bb6347bf1ac..6e5b532b58a 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -18,6 +18,7 @@ const configuredWsUrl = process.env.VITE_WS_URL?.trim(); const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || ""; +const configuredClerkCliOAuthClientId = repoEnv.VITE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() || ""; const configuredRelayTracingUrl = repoEnv.VITE_RELAY_OTLP_TRACES_URL?.trim() || ""; const configuredRelayTracingDataset = repoEnv.VITE_RELAY_OTLP_TRACES_DATASET?.trim() || ""; const configuredRelayTracingToken = repoEnv.VITE_RELAY_OTLP_TRACES_TOKEN?.trim() || ""; @@ -121,6 +122,9 @@ export default defineConfig(() => { "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate), + "import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID": JSON.stringify( + configuredClerkCliOAuthClientId, + ), "import.meta.env.VITE_RELAY_OTLP_TRACES_URL": JSON.stringify(configuredRelayTracingUrl), "import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET": JSON.stringify( configuredRelayTracingDataset, diff --git a/packages/shared/package.json b/packages/shared/package.json index bf68ce766ef..9c45753cd8e 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -135,6 +135,10 @@ "types": "./src/cliArgs.ts", "import": "./src/cliArgs.ts" }, + "./connectAuth": { + "types": "./src/connectAuth.ts", + "import": "./src/connectAuth.ts" + }, "./path": { "types": "./src/path.ts", "import": "./src/path.ts" diff --git a/packages/shared/src/connectAuth.test.ts b/packages/shared/src/connectAuth.test.ts new file mode 100644 index 00000000000..9ffef3936bb --- /dev/null +++ b/packages/shared/src/connectAuth.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + buildConnectAuthorizeRequestUrl, + buildConnectClerkAuthorizeUrl, + connectCallbackUrl, + encodeConnectAuthCode, + parseConnectAuthCode, + readConnectAuthorizeRequest, +} from "./connectAuth.ts"; + +describe("connectAuth", () => { + it("round-trips state and challenge through the authorize URL fragment", () => { + const url = buildConnectAuthorizeRequestUrl({ + hostedAppUrl: "https://app.t3.codes", + state: "q7mK9xV2pL4nR8sT6wYzAQ", + challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + }); + const parsed = new URL(url); + + expect(parsed.origin).toBe("https://app.t3.codes"); + expect(parsed.pathname).toBe("/connect"); + expect(parsed.search).toBe(""); + expect(readConnectAuthorizeRequest(parsed)).toEqual({ + state: "q7mK9xV2pL4nR8sT6wYzAQ", + challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", + }); + }); + + it("rejects authorize requests missing state or challenge", () => { + expect(readConnectAuthorizeRequest(new URL("https://app.t3.codes/connect"))).toBeNull(); + expect( + readConnectAuthorizeRequest(new URL("https://app.t3.codes/connect#state=abc")), + ).toBeNull(); + expect( + readConnectAuthorizeRequest(new URL("https://app.t3.codes/connect#challenge=abc")), + ).toBeNull(); + }); + + it("builds a PKCE authorize URL against the Clerk endpoint", () => { + const url = new URL( + buildConnectClerkAuthorizeUrl({ + authorizationEndpoint: "https://clerk.t3.codes/oauth/authorize", + clientId: "oauthapp_123", + redirectUri: connectCallbackUrl("https://app.t3.codes"), + scopes: ["openid", "profile", "email"], + state: "state-1", + challenge: "challenge-1", + }), + ); + + expect(url.origin).toBe("https://clerk.t3.codes"); + expect(url.pathname).toBe("/oauth/authorize"); + expect(url.searchParams.get("client_id")).toBe("oauthapp_123"); + expect(url.searchParams.get("redirect_uri")).toBe("https://app.t3.codes/connect/callback"); + expect(url.searchParams.get("response_type")).toBe("code"); + expect(url.searchParams.get("scope")).toBe("openid profile email"); + expect(url.searchParams.get("state")).toBe("state-1"); + expect(url.searchParams.get("code_challenge")).toBe("challenge-1"); + expect(url.searchParams.get("code_challenge_method")).toBe("S256"); + }); + + it("round-trips the out-of-band authorization code and preserves dots inside it", () => { + const blob = encodeConnectAuthCode({ code: "az9.code.chunk", state: "state-uuid" }); + expect(parseConnectAuthCode(blob)).toEqual({ code: "az9.code.chunk", state: "state-uuid" }); + expect(parseConnectAuthCode(` ${blob}\n`)).toEqual({ + code: "az9.code.chunk", + state: "state-uuid", + }); + }); + + it("rejects malformed out-of-band authorization codes", () => { + expect(parseConnectAuthCode("")).toBeNull(); + expect(parseConnectAuthCode("no-separator")).toBeNull(); + expect(parseConnectAuthCode(".leading")).toBeNull(); + expect(parseConnectAuthCode("trailing.")).toBeNull(); + }); +}); diff --git a/packages/shared/src/connectAuth.ts b/packages/shared/src/connectAuth.ts new file mode 100644 index 00000000000..8d849d77ae6 --- /dev/null +++ b/packages/shared/src/connectAuth.ts @@ -0,0 +1,124 @@ +import { readHashParams } from "./remote.ts"; + +const CONNECT_AUTH_STATE_PARAM = "state"; +const CONNECT_AUTH_CHALLENGE_PARAM = "challenge"; +const CONNECT_AUTH_CODE_SEPARATOR = "."; + +const CONNECT_AUTHORIZE_PATH = "/connect"; +const CONNECT_CALLBACK_PATH = "/connect/callback"; + +/** + * The CLI prints URLs against this origin and the web bundle uses it to + * decide whether it is the hosted deployment — the two must agree, so the + * default lives here. + */ +export const DEFAULT_HOSTED_APP_URL = "https://app.t3.codes"; + +/** + * Requested at authorize time by the hosted page and honored by the CLI's + * token exchange; keep both sides on this single definition. + */ +export const CONNECT_OAUTH_SCOPES = ["openid", "profile", "email"] as const; + +export interface ConnectAuthorizeRequest { + readonly state: string; + readonly challenge: string; +} + +/** + * The URL a headless CLI prints for the user to open on a machine with a + * browser. `state` and `code_challenge` ride the fragment so they never reach + * the hosted app's server or CDN logs; neither is a secret. + */ +export function buildConnectAuthorizeRequestUrl(input: { + readonly hostedAppUrl: string; + readonly state: string; + readonly challenge: string; +}): string { + const url = new URL(CONNECT_AUTHORIZE_PATH, input.hostedAppUrl); + url.hash = new URLSearchParams([ + [CONNECT_AUTH_STATE_PARAM, input.state], + [CONNECT_AUTH_CHALLENGE_PARAM, input.challenge], + ]).toString(); + return url.toString(); +} + +export function readConnectAuthorizeRequest(url: URL): ConnectAuthorizeRequest | null { + const params = readHashParams(url); + const state = params.get(CONNECT_AUTH_STATE_PARAM)?.trim() ?? ""; + const challenge = params.get(CONNECT_AUTH_CHALLENGE_PARAM)?.trim() ?? ""; + if (!state || !challenge) { + return null; + } + return { state, challenge }; +} + +export function connectCallbackUrl(hostedAppUrl: string): string { + return new URL(CONNECT_CALLBACK_PATH, hostedAppUrl).toString(); +} + +export function buildConnectClerkAuthorizeUrl(input: { + readonly authorizationEndpoint: string; + readonly clientId: string; + readonly redirectUri: string; + readonly scopes: ReadonlyArray; + readonly state: string; + readonly challenge: string; +}): string { + const url = new URL(input.authorizationEndpoint); + url.searchParams.set("client_id", input.clientId); + url.searchParams.set("redirect_uri", input.redirectUri); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", input.scopes.join(" ")); + url.searchParams.set("state", input.state); + url.searchParams.set("code_challenge", input.challenge); + url.searchParams.set("code_challenge_method", "S256"); + return url.toString(); +} + +export interface ConnectAuthCode { + readonly code: string; + readonly state: string; +} + +/** + * The single blob the hosted callback page displays and the CLI accepts. + * Bundling `state` with the authorization code lets the CLI keep the loopback + * flow's CSRF check without any backend: it verifies the returned state + * matches the one it generated. Clerk authorization codes and the CLI's + * base64url states never contain ".". + */ +export function encodeConnectAuthCode(input: ConnectAuthCode): string { + return `${input.code}${CONNECT_AUTH_CODE_SEPARATOR}${input.state}`; +} + +/** + * Validates an out-of-band authorization code against the state of the request this process + * generated. Returns the parsed code or a user-facing error message; both + * the prompt's live validation and the authoritative post-prompt check go + * through here so they cannot drift. + */ +export function checkConnectAuthCode( + blob: string, + expectedState: string, +): ConnectAuthCode | string { + const parsed = parseConnectAuthCode(blob); + if (parsed === null) { + return "That does not look like a T3 Connect code. Copy the full code."; + } + if (parsed.state !== expectedState) { + return "That code belongs to a different connect request. Open the URL above and try again."; + } + return parsed; +} + +export function parseConnectAuthCode(blob: string): ConnectAuthCode | null { + const trimmed = blob.trim(); + const separatorIndex = trimmed.lastIndexOf(CONNECT_AUTH_CODE_SEPARATOR); + if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) { + return null; + } + const code = trimmed.slice(0, separatorIndex); + const state = trimmed.slice(separatorIndex + 1); + return { code, state }; +} diff --git a/packages/shared/src/hostProcess.ts b/packages/shared/src/hostProcess.ts index 1e5b69749cf..baff2c5ffed 100644 --- a/packages/shared/src/hostProcess.ts +++ b/packages/shared/src/hostProcess.ts @@ -30,4 +30,18 @@ export const HostProcessEnvironment = Context.Reference( }, ); +export const HostProcessExecutablePath = Context.Reference( + "@t3tools/shared/hostProcess/HostProcessExecutablePath", + { + defaultValue: () => process.execPath, + }, +); + +export const HostProcessArguments = Context.Reference>( + "@t3tools/shared/hostProcess/HostProcessArguments", + { + defaultValue: () => process.argv, + }, +); + export const isHostWindows = Effect.map(HostProcessPlatform, (platform) => platform === "win32"); diff --git a/packages/shared/src/remote.ts b/packages/shared/src/remote.ts index 0adfc6fe90a..1b5d1c586e2 100644 --- a/packages/shared/src/remote.ts +++ b/packages/shared/src/remote.ts @@ -5,7 +5,7 @@ const HOSTED_PAIRING_HOST_PARAM = "host"; const HOSTED_PAIRING_LABEL_PARAM = "label"; const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]); -const readHashParams = (url: URL): URLSearchParams => +export const readHashParams = (url: URL): URLSearchParams => new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass()( diff --git a/scripts/lib/public-config.ts b/scripts/lib/public-config.ts index 9b988df9c5b..36ca5497f80 100644 --- a/scripts/lib/public-config.ts +++ b/scripts/lib/public-config.ts @@ -55,6 +55,7 @@ export function loadRepoEnv({ ...(config.clerkCliOAuthClientId ? { T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: config.clerkCliOAuthClientId, + VITE_CLERK_CLI_OAUTH_CLIENT_ID: config.clerkCliOAuthClientId, } : {}), ...(config.relayUrl @@ -116,7 +117,11 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP "VITE_CLERK_JWT_TEMPLATE", "EXPO_PUBLIC_CLERK_JWT_TEMPLATE", ), - clerkCliOAuthClientId: firstNonEmpty(sources, "T3CODE_CLERK_CLI_OAUTH_CLIENT_ID"), + clerkCliOAuthClientId: firstNonEmpty( + sources, + "T3CODE_CLERK_CLI_OAUTH_CLIENT_ID", + "VITE_CLERK_CLI_OAUTH_CLIENT_ID", + ), relayUrl: firstNonEmpty(sources, "T3CODE_RELAY_URL", "VITE_T3CODE_RELAY_URL"), mobileOtlpTracesUrl: firstNonEmpty( sources, From 2b180a2b2d307a902fa63c8de3d7a42ea5e1b9c5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 19 Jul 2026 19:23:53 +0200 Subject: [PATCH 05/39] Refine T3 Connect authorization surfaces (#4159) Co-authored-by: codex --- apps/server/src/cloud/cliAuthHtml.test.ts | 24 ++++++- apps/server/src/cloud/cliAuthHtml.ts | 85 +++++++++++++---------- apps/server/vite.config.ts | 3 + apps/web/src/branding.logic.ts | 4 ++ apps/web/src/branding.test.ts | 11 +++ 5 files changed, 89 insertions(+), 38 deletions(-) diff --git a/apps/server/src/cloud/cliAuthHtml.test.ts b/apps/server/src/cloud/cliAuthHtml.test.ts index a49eaffeda7..1104927b980 100644 --- a/apps/server/src/cloud/cliAuthHtml.test.ts +++ b/apps/server/src/cloud/cliAuthHtml.test.ts @@ -1,13 +1,31 @@ import { expect, it } from "@effect/vitest"; -import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts"; +import { + renderLoopbackAuthorizationCompleteHtml, + resolveLoopbackAuthorizationStage, +} from "./cliAuthHtml.ts"; it("renders the branded loopback authorization completion page", () => { const html = renderLoopbackAuthorizationCompleteHtml(); - expect(html).toContain("T3 Code"); + expect(resolveLoopbackAuthorizationStage()).toBe("dev"); + expect(html).toContain("T3 Code (Dev)"); + expect(html).toContain('class="stage stage-dev"'); expect(html).not.toContain("Secure terminal handoff"); expect(html).toContain("You're connected"); - expect(html).toContain("return to the terminal"); + expect(html).toContain("Return to your terminal"); + expect(html).not.toContain('class="next"'); expect(html).toContain('name="viewport"'); + expect(html).not.toContain('class="status"'); +}); + +it("renders the matching header treatment for each release channel", () => { + const nightly = renderLoopbackAuthorizationCompleteHtml("nightly"); + const latest = renderLoopbackAuthorizationCompleteHtml("latest"); + + expect(nightly).toContain("T3 Code (Nightly)"); + expect(nightly).toContain('class="stage stage-nightly"'); + expect(latest).toContain('

T3 Code

'); + expect(latest).not.toContain("(Latest)"); + expect(latest).toContain('class="stage stage-latest"'); }); diff --git a/apps/server/src/cloud/cliAuthHtml.ts b/apps/server/src/cloud/cliAuthHtml.ts index 3c4141fba44..5a22a25993a 100644 --- a/apps/server/src/cloud/cliAuthHtml.ts +++ b/apps/server/src/cloud/cliAuthHtml.ts @@ -1,4 +1,22 @@ -export function renderLoopbackAuthorizationCompleteHtml(): string { +export type LoopbackAuthorizationStage = "dev" | "nightly" | "latest"; + +declare const __T3CODE_BUILD_CHANNEL__: "nightly" | "latest" | undefined; + +export function resolveLoopbackAuthorizationStage(): LoopbackAuthorizationStage { + return typeof __T3CODE_BUILD_CHANNEL__ === "undefined" ? "dev" : __T3CODE_BUILD_CHANNEL__; +} + +const stageBrands = { + dev: "T3 Code (Dev)", + nightly: "T3 Code (Nightly)", + latest: "T3 Code", +} as const satisfies Record; + +export function renderLoopbackAuthorizationCompleteHtml( + stage: LoopbackAuthorizationStage = resolveLoopbackAuthorizationStage(), +): string { + const stageBrand = stageBrands[stage]; + return ` @@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { overflow: hidden; padding: 22px 24px; color: white; + } + .stage-latest { + background: + radial-gradient(circle at 76% 18%, rgba(136, 204, 255, 0.52), transparent 38%), + linear-gradient(135deg, #2468df, #172f82); + } + .stage-dev { background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%); } - .stage::before { + .stage-dev::before { content: ""; position: absolute; inset: 0; @@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px); background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px; } + .stage-nightly { + background: + radial-gradient(22rem 8rem at 78% 18%, rgba(81, 101, 216, 0.42), transparent 58%), + linear-gradient(145deg, #07152f 0%, #151443 52%, #32155b 100%); + } + .stage-nightly::before { + content: ""; + position: absolute; + inset: 0; + opacity: 0.78; + background-image: + radial-gradient(circle at 12px 12px, rgba(228, 234, 255, 0.9) 0 1px, transparent 1.5px), + radial-gradient(circle at 38px 28px, rgba(228, 234, 255, 0.58) 0 0.8px, transparent 1.3px), + radial-gradient(circle at 58px 9px, rgba(200, 215, 255, 0.72) 0 0.9px, transparent 1.4px); + background-size: 72px 48px, 96px 64px, 128px 56px; + } .stage::after { content: ""; position: absolute; @@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { .stage-content { position: relative; z-index: 1; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; } .brand { margin: 0; @@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { } .brand { color: rgba(255, 255, 255, 0.82); } .content { padding: 30px 32px 34px; } - .status { - width: 42px; - height: 42px; - display: grid; - place-items: center; - margin-top: -52px; - margin-bottom: 22px; - border: 4px solid white; - border-radius: 50%; - background: #2065df; - color: white; - font-size: 22px; - font-weight: 700; - box-shadow: 0 8px 22px rgba(25, 72, 177, 0.3); - } .eyebrow { margin: 0 0 8px; color: #2866cc; @@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { } h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; } .description { margin: 12px 0 0; color: #646975; font-size: 15px; line-height: 1.6; } - .next { - margin-top: 24px; - padding: 14px 16px; - border: 1px solid rgba(23, 25, 31, 0.1); - border-radius: 12px; - background: #f7f8fa; - color: #454a55; - font-size: 13px; - } - .next strong { color: #17191f; } @media (prefers-color-scheme: dark) { :root { background: #101115; color: #f1f3f7; } body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; } main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); } - .status { border-color: #191b21; } .eyebrow { color: #77a8ff; } .description { color: #a8adb8; } - .next { border-color: rgba(255, 255, 255, 0.1); background: #20232a; color: #b9bec8; } - .next strong { color: #f1f3f7; } }
-
+
-

T3 Code

+

${stageBrand}

-

Browser authorization complete

You're connected

-

The authorization code was delivered securely to your waiting terminal.

- +

Return to your terminal to finish setting up T3 Connect. You can close this window.

diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 473df069ed7..521654f3279 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus"; import baseConfig from "../../vite.config.ts"; import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; +import packageJson from "./package.json" with { type: "json" }; const bundledPackagePrefixes = [ "@pierre/diffs", @@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean { } const repoEnv = loadRepoEnv(); +const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest"; export default mergeConfig( baseConfig, @@ -42,6 +44,7 @@ export default mergeConfig( js: "#!/usr/bin/env node\n", }, define: { + __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel), __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""), __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify( repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "", diff --git a/apps/web/src/branding.logic.ts b/apps/web/src/branding.logic.ts index b87276f1b9c..056fbb76e6a 100644 --- a/apps/web/src/branding.logic.ts +++ b/apps/web/src/branding.logic.ts @@ -4,6 +4,10 @@ export function formatAppDisplayName(input: { readonly baseName: string; readonly stageLabel: string; }): string { + if (input.stageLabel.trim().toLowerCase() === "latest") { + return input.baseName; + } + return `${input.baseName} (${input.stageLabel})`; } diff --git a/apps/web/src/branding.test.ts b/apps/web/src/branding.test.ts index 4aa969c0279..e1c87bcf059 100644 --- a/apps/web/src/branding.test.ts +++ b/apps/web/src/branding.test.ts @@ -50,6 +50,17 @@ describe("branding", () => { expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)"); }); + it("does not label the latest hosted app channel", async () => { + vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "latest"); + + const branding = await import("./branding"); + + expect(branding.HOSTED_APP_CHANNEL).toBe("latest"); + expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest"); + expect(branding.APP_STAGE_LABEL).toBe("Latest"); + expect(branding.APP_DISPLAY_NAME).toBe("T3 Code"); + }); + it("ignores unknown hosted app channels", async () => { vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview"); From 398140a9bde5596cd6b45dc546150c6f5e3b23b7 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:03:10 +0530 Subject: [PATCH 06/39] fix: increase OpenCode server startup timeout from 5s to 30s (#4132) --- apps/server/src/provider/opencodeRuntime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/provider/opencodeRuntime.ts b/apps/server/src/provider/opencodeRuntime.ts index a83c134d5bd..46d0b3019bd 100644 --- a/apps/server/src/provider/opencodeRuntime.ts +++ b/apps/server/src/provider/opencodeRuntime.ts @@ -37,7 +37,7 @@ const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJ const OPENCODE_EMPTY_CONFIG_CONTENT = "{}"; const OPENCODE_SERVER_READY_PREFIX = "opencode server listening"; -const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000; +const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 30_000; const DEFAULT_HOSTNAME = "127.0.0.1"; export interface OpenCodeServerProcess { readonly url: string; From e8ff6bc7f829911faf23074b147f95d0470d518d Mon Sep 17 00:00:00 2001 From: Kriday Dave Date: Sun, 19 Jul 2026 23:07:22 +0530 Subject: [PATCH 07/39] fix(shared): delete unused agentAwareness phase predicates (#4134) --- packages/shared/src/agentAwareness.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/shared/src/agentAwareness.ts b/packages/shared/src/agentAwareness.ts index 248b983cd4f..c0f5842eb7c 100644 --- a/packages/shared/src/agentAwareness.ts +++ b/packages/shared/src/agentAwareness.ts @@ -50,14 +50,6 @@ export function buildAgentAwarenessDeepLink(input: { return `/threads/${encodeURIComponent(input.environmentId)}/${encodeURIComponent(input.threadId)}`; } -export function isTerminalAgentAwarenessPhase(phase: AgentAwarenessPhase): boolean { - return phase === "completed" || phase === "failed"; -} - -export function isInterruptiveAgentAwarenessPhase(phase: AgentAwarenessPhase): boolean { - return phase === "waiting_for_approval" || phase === "waiting_for_input" || phase === "failed"; -} - export function projectThreadAwareness( input: ProjectThreadAwarenessInput, ): AgentAwarenessState | null { From 271454cbd2f93f25c8c158c0bb247164a0041578 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 19 Jul 2026 19:40:22 +0200 Subject: [PATCH 08/39] fix(mobile): Stabilize native stack option updates (#4037) Co-authored-by: codex --- apps/mobile/src/native/StackHeader.tsx | 103 +++++++++++++++++- ...act-navigation%2Fnative-stack@7.17.6.patch | 2 + pnpm-lock.yaml | 6 +- 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index bbcbb8e4282..8a8c355b760 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -11,6 +11,7 @@ import { useEffect, useLayoutEffect, useMemo, + useRef, type ReactElement, type ReactNode, } from "react"; @@ -61,20 +62,116 @@ function normalizeScreenOptions( return normalized as NativeStackNavigationOptions; } +function optionsSignature(value: unknown, seen = new WeakSet()): string { + if (value === null) return "null"; + switch (typeof value) { + case "boolean": + case "number": + case "string": + return JSON.stringify(value); + case "undefined": + return "undefined"; + case "function": + // Header factories are frequently recreated inline. Their source is + // stable across equivalent renders, while a reference comparison would + // make navigation.setOptions re-enter the navigator indefinitely. + return `function:${Function.prototype.toString.call(value)}`; + case "symbol": + return `symbol:${String(value)}`; + case "bigint": + return `bigint:${String(value)}`; + case "object": { + const object = value as object; + if (seen.has(object)) return "[circular]"; + seen.add(object); + if (Array.isArray(value)) { + return `[${value.map((entry) => optionsSignature(entry, seen)).join(",")}]`; + } + // React refs carry mutable native instances that must not make static + // screen options appear different after every render. + if ("current" in object) return "[ref]"; + return `{${Object.keys(value as Record) + .sort() + .map( + (key) => + `${JSON.stringify(key)}:${optionsSignature((value as Record)[key], seen)}`, + ) + .join(",")}}`; + } + } + return String(value); +} + +function stabilizeOptionFunctions( + value: unknown, + path: string, + latestFunctions: Map unknown>, + wrappers: Map unknown>, + seen = new WeakSet(), +): unknown { + if (typeof value === "function") { + latestFunctions.set(path, value as (...args: unknown[]) => unknown); + let wrapper = wrappers.get(path); + if (!wrapper) { + wrapper = (...args: unknown[]) => { + return latestFunctions.get(path)?.(...args); + }; + wrappers.set(path, wrapper); + } + return wrapper; + } + if (Array.isArray(value)) { + if (seen.has(value)) return value; + seen.add(value); + return value.map((entry, index) => + stabilizeOptionFunctions(entry, `${path}[${index}]`, latestFunctions, wrappers, seen), + ); + } + if (value !== null && typeof value === "object") { + if (seen.has(value) || "current" in value) return value; + seen.add(value); + return Object.fromEntries( + Object.entries(value as Record).map(([key, entry]) => [ + key, + stabilizeOptionFunctions(entry, `${path}.${key}`, latestFunctions, wrappers, seen), + ]), + ); + } + return value; +} + export function NativeStackScreenOptions(props: { readonly options?: AppNativeStackNavigationOptions; readonly listeners?: Record void>; readonly name?: string; }) { const navigation = useNativeStackNavigation(); + const lastAppliedOptionsSignatureRef = useRef(undefined); + const latestOptionFunctionsRef = useRef(new Map unknown>()); + const optionFunctionWrappersRef = useRef(new Map unknown>()); const normalizedOptions = useMemo(() => normalizeScreenOptions(props.options), [props.options]); + const stableOptions = normalizedOptions + ? (stabilizeOptionFunctions( + normalizedOptions, + "options", + latestOptionFunctionsRef.current, + optionFunctionWrappersRef.current, + ) as NativeStackNavigationOptions) + : undefined; useLayoutEffect(() => { - if (!navigation || !normalizedOptions) { + if (!navigation || !stableOptions) { + return; + } + const signature = optionsSignature(stableOptions); + // Avoid re-entering navigation state when semantically equal options are + // reapplied every layout (common when callers pass unstable object literals). + if (lastAppliedOptionsSignatureRef.current === signature) { return; } - navigation.setOptions(normalizedOptions); - }, [navigation, normalizedOptions]); + lastAppliedOptionsSignatureRef.current = signature; + navigation.setOptions(stableOptions); + }, [navigation, stableOptions]); useEffect(() => { if (!navigation || !props.listeners) { diff --git a/patches/@react-navigation%2Fnative-stack@7.17.6.patch b/patches/@react-navigation%2Fnative-stack@7.17.6.patch index cd2b86fb76c..1ec4d978529 100644 --- a/patches/@react-navigation%2Fnative-stack@7.17.6.patch +++ b/patches/@react-navigation%2Fnative-stack@7.17.6.patch @@ -1,3 +1,5 @@ +diff --git a/lib/module/views/useHeaderConfigProps.js b/lib/module/views/useHeaderConfigProps.js +index 0b75c70b4e0d233ee3b5faaf9cfbc40d4f8ed494..eb174e3fde91a7783f132b3fb16b01175ec19220 100644 --- a/lib/module/views/useHeaderConfigProps.js +++ b/lib/module/views/useHeaderConfigProps.js @@ -19,6 +19,12 @@ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0271c378ae..c58ccffc420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ patchedDependencies: '@legendapp/list@3.2.0': 45e4cbcbeeca1b628b8480869374d7cbc77e3fbfa97ee0107a0d72c3c4a5d7f3 '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae - '@react-navigation/native-stack@7.17.6': 2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca + '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f react-native-gesture-handler@2.31.2: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 @@ -229,7 +229,7 @@ importers: version: 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@react-navigation/native-stack': specifier: 7.17.6 - version: 7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(a49e8e72dc3ef754b9d26038db8e6d3f) + version: 7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(a49e8e72dc3ef754b9d26038db8e6d3f) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -14148,7 +14148,7 @@ snapshots: optionalDependencies: '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@react-navigation/native-stack@7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(a49e8e72dc3ef754b9d26038db8e6d3f)': + '@react-navigation/native-stack@7.17.6(patch_hash=c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273)(a49e8e72dc3ef754b9d26038db8e6d3f)': dependencies: '@react-navigation/elements': 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) From 69c8be2ee54e3aff9c90b98d8e10aeafd0aaae2e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 19 Jul 2026 20:32:36 +0200 Subject: [PATCH 09/39] Make test-t3-app skill discoverable by Claude Code (#4162) Co-authored-by: Claude Sonnet 5 --- .claude/skills | 1 + 1 file changed, 1 insertion(+) create mode 120000 .claude/skills diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000000..2b7a412b8fa --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file From b511227b7ad421c422f1ebca65116776020e4799 Mon Sep 17 00:00:00 2001 From: maria Date: Sun, 19 Jul 2026 17:02:23 -0400 Subject: [PATCH 10/39] fix(web): improve dev sidebar backdrop contrast & remove version pills (#4166) --- apps/web/src/components/Sidebar.tsx | 14 ++------------ apps/web/src/components/SidebarStageBackdrop.tsx | 4 ++-- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 38e9f7a828a..715a94e9672 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2762,12 +2762,12 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({ backdropVariant && "hover:bg-white/15 [&_svg]:text-white/85! [&_svg]:hover:text-white!", )} /> - + ); }); -function SidebarBrand({ stageLabel, onBackdrop }: { stageLabel: string; onBackdrop: boolean }) { +function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { return ( Code - - {stageLabel} - ); } diff --git a/apps/web/src/components/SidebarStageBackdrop.tsx b/apps/web/src/components/SidebarStageBackdrop.tsx index d3d86186c03..6f0953a9773 100644 --- a/apps/web/src/components/SidebarStageBackdrop.tsx +++ b/apps/web/src/components/SidebarStageBackdrop.tsx @@ -201,9 +201,9 @@ function DevBlueprintArt() { gradientUnits="userSpaceOnUse" spreadMethod="reflect" > - + - + Date: Mon, 20 Jul 2026 01:24:57 +0200 Subject: [PATCH 11/39] Fix draft banner stack overlap (#4164) Co-authored-by: codex --- .../chat/ComposerBannerStack.test.tsx | 37 +++++++++++++++ .../components/chat/ComposerBannerStack.tsx | 47 +++++++++++-------- .../src/components/chat/DraftHeroHeadline.tsx | 2 +- 3 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerBannerStack.test.tsx diff --git a/apps/web/src/components/chat/ComposerBannerStack.test.tsx b/apps/web/src/components/chat/ComposerBannerStack.test.tsx new file mode 100644 index 00000000000..16fdff64425 --- /dev/null +++ b/apps/web/src/components/chat/ComposerBannerStack.test.tsx @@ -0,0 +1,37 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vite-plus/test"; + +import { ComposerBannerStack, type ComposerBannerStackItem } from "./ComposerBannerStack"; + +const banner = (id: string): ComposerBannerStackItem => ({ + id, + variant: "warning", + icon: , + title: `${id} warning`, +}); + +describe("ComposerBannerStack", () => { + it("keeps expanded banners in layout flow so surrounding content moves out of their way", () => { + const markup = renderToStaticMarkup( + , + ); + + const expandedItems = markup.match( + /
/, + ); + + expect(expandedItems?.[1]).toContain("grid-rows-[0fr]"); + expect(expandedItems?.[1]).toContain("group-hover/banner-stack:grid-rows-[1fr]"); + expect(expandedItems?.[1]).toContain("z-20"); + expect(expandedItems?.[1]).not.toContain("absolute"); + expect(markup.indexOf("front warning")).toBeLessThan(markup.indexOf("stacked warning")); + expect(markup).toContain("invisible pointer-events-none"); + expect(markup).toContain("group-focus-within/banner-stack:visible"); + }); + + it("does not render an expandable region for a single banner", () => { + const markup = renderToStaticMarkup(); + + expect(markup).not.toContain("data-composer-banner-stack-expanded-items"); + }); +}); diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index 1f82c452b05..d51c23a2f27 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -85,7 +85,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
@@ -119,30 +119,39 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
{hasStack ? (
- {stackedItems.map((item) => ( +
- requestDismiss(item)} - /> + {stackedItems.map((item) => ( +
+ requestDismiss(item)} + /> +
+ ))}
- ))} +
) : null}
diff --git a/apps/web/src/components/chat/DraftHeroHeadline.tsx b/apps/web/src/components/chat/DraftHeroHeadline.tsx index 32d195d7b2e..6a88fb8da19 100644 --- a/apps/web/src/components/chat/DraftHeroHeadline.tsx +++ b/apps/web/src/components/chat/DraftHeroHeadline.tsx @@ -101,7 +101,7 @@ export function DraftHeroHeadline({ ); return ( -

+

{hasResolvedProject ? ( <>What should we build in {projectSelector}? ) : canChooseProject ? ( From e34250df469dcf50b444777f903b5f3482b4ce3c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 20 Jul 2026 01:32:19 +0200 Subject: [PATCH 12/39] Add portable mobile app testing guidance (#4165) Co-authored-by: codex --- .agents/skills/ios-debugger-agent/LICENSE | 21 ++ .agents/skills/ios-debugger-agent/SKILL.md | 64 ++++++ .../ios-debugger-agent/agents/openai.yaml | 9 + .agents/skills/ios-simulator-browser/LICENSE | 21 ++ .agents/skills/ios-simulator-browser/SKILL.md | 51 +++++ .../ios-simulator-browser/agents/openai.yaml | 4 + .agents/skills/test-t3-app/SKILL.md | 2 + .agents/skills/test-t3-mobile/SKILL.md | 188 ++++++++++++++++++ .../skills/test-t3-mobile/agents/openai.yaml | 4 + .codex/config.toml | 9 + .mcp.json | 11 + AGENTS.md | 6 +- .../connection/ConnectionSheetButton.tsx | 3 + 13 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 .agents/skills/ios-debugger-agent/LICENSE create mode 100644 .agents/skills/ios-debugger-agent/SKILL.md create mode 100644 .agents/skills/ios-debugger-agent/agents/openai.yaml create mode 100644 .agents/skills/ios-simulator-browser/LICENSE create mode 100644 .agents/skills/ios-simulator-browser/SKILL.md create mode 100644 .agents/skills/ios-simulator-browser/agents/openai.yaml create mode 100644 .agents/skills/test-t3-mobile/SKILL.md create mode 100644 .agents/skills/test-t3-mobile/agents/openai.yaml create mode 100644 .codex/config.toml create mode 100644 .mcp.json diff --git a/.agents/skills/ios-debugger-agent/LICENSE b/.agents/skills/ios-debugger-agent/LICENSE new file mode 100644 index 00000000000..0d193abe04f --- /dev/null +++ b/.agents/skills/ios-debugger-agent/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) OpenAI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/ios-debugger-agent/SKILL.md b/.agents/skills/ios-debugger-agent/SKILL.md new file mode 100644 index 00000000000..7afa579383d --- /dev/null +++ b/.agents/skills/ios-debugger-agent/SKILL.md @@ -0,0 +1,64 @@ +--- +name: ios-debugger-agent +description: Build, launch, inspect, and drive iOS apps with the repository-configured XcodeBuildMCP server. Use on macOS for iOS Simulator builds, focused native test runs, semantic UI automation, screenshots, logs, or debugging, including T3 Code Mobile verification. +--- + +# iOS Debugger Agent + +Use the repository-configured `xcodebuildmcp` tools instead of requiring a globally installed Codex plugin. Prefer MCP tools over raw `xcodebuild`, `xcrun`, or `simctl` when the client exposes them. + +## Confirm availability + +This workflow requires macOS 14.5 or newer, Xcode 16 or newer, and Node.js 18 or newer. The repository pins XcodeBuildMCP in both `.mcp.json` for Claude Code and `.codex/config.toml` for Codex. Project MCP servers may require one-time trust or approval followed by a new session. + +If the tools are missing: + +1. Confirm the repository is trusted and its project MCP server was approved. +2. Restart or recreate the agent session after approving configuration. +3. Run `npx --yes xcodebuildmcp@2.6.2 doctor` when the server starts but simulator or UI-automation tools are unavailable. Follow its actionable Xcode or AXe setup guidance. +4. Fall back to the pinned XcodeBuildMCP CLI or native Apple CLIs only when the current agent client cannot expose project MCP tools. + +Do not ask contributors to install the OpenAI `build-ios-apps` plugin globally. + +## Establish one simulator context + +1. Call `session_show_defaults` before discovery, build, launch, or UI work. +2. Call `list_sims` and select one explicit simulator UDID. Prefer a simulator that is already booted; boot an installed simulator when verification requires it, but do not create or download runtimes without user authorization. +3. Call `session_set_defaults` with the project or workspace, scheme, Debug configuration, simulator ID, and bundle identifier when known. +4. Keep every subsequent build, launch, screenshot, log capture, and UI action pinned to that same UDID. + +Avoid generic Mac window automation for switching among Simulator windows. Explicit device identity is more reliable. + +## Choose build or launch + +- Use `build_run_sim` when native source, native dependencies, entitlements, or project configuration changed. +- Use `test_sim` for the smallest relevant native test target or test cases; do not run an entire workspace test matrix routinely. +- Use `launch_app_sim` when a compatible app is already installed and no native rebuild is needed. +- To reuse an existing build artifact, use `get_sim_app_path` or `get_app_bundle_id`, install it with `install_app_sim` when necessary, and then launch it. +- Do not run a build-only action immediately before `build_run_sim` unless the task requires both artifacts. + +After launch, call `snapshot_ui` or `screenshot` before interacting. An open Simulator window alone is not evidence that the intended app launched. + +## Drive the UI semantically + +1. Call `snapshot_ui` to obtain the current accessibility hierarchy and element references. +2. Use only current `elementRef` values whose snapshot entries list the intended action. XcodeBuildMCP `2.6.2` does not accept coordinates for `tap`; when the app exposes no actionable reference, prefer a registered deep link or another app-supported route and otherwise report the accessibility blocker. +3. Refresh with `snapshot_ui` after navigation or layout changes. Element references are snapshot-specific. +4. Use `wait_for_ui` for asynchronous transitions when available rather than fixed sleeps. +5. Capture a final `screenshot` for the state that proves the affected flow. + +Use `gesture` or scoped swipe actions when needed. If a gesture is unreliable, return to a known route or relaunch rather than switching to generic desktop automation. + +## Capture logs and debug + +- Use `start_sim_log_cap` and `stop_sim_log_cap` with the exact bundle identifier for focused runtime logs. +- Use debugger tools only when the task requires runtime diagnosis; attach to the selected simulator and app rather than an ambiguous process. +- Summarize relevant errors instead of returning unbounded logs. + +## Clean up + +Stop only log captures, debugger sessions, apps, or simulators started for the current test. Leave pre-existing simulators and unrelated sessions alone. + +## Upstream + +Adapted from OpenAI's [`build-ios-apps`](https://github.com/openai/plugins/tree/main/plugins/build-ios-apps) plugin version `0.1.2` (`ios-debugger-agent`, MIT) and aligned with XcodeBuildMCP `2.6.2` tool names. diff --git a/.agents/skills/ios-debugger-agent/agents/openai.yaml b/.agents/skills/ios-debugger-agent/agents/openai.yaml new file mode 100644 index 00000000000..094bae6620b --- /dev/null +++ b/.agents/skills/ios-debugger-agent/agents/openai.yaml @@ -0,0 +1,9 @@ +interface: + display_name: "iOS Debugger Agent" + short_description: "Build and drive iOS Simulator apps" + default_prompt: "Use $ios-debugger-agent to build, launch, and inspect the current iOS app on Simulator." +dependencies: + tools: + - type: "mcp" + value: "xcodebuildmcp" + description: "Repository-configured Xcode build, simulator, logging, debugging, and semantic UI tools" diff --git a/.agents/skills/ios-simulator-browser/LICENSE b/.agents/skills/ios-simulator-browser/LICENSE new file mode 100644 index 00000000000..0d193abe04f --- /dev/null +++ b/.agents/skills/ios-simulator-browser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) OpenAI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.agents/skills/ios-simulator-browser/SKILL.md b/.agents/skills/ios-simulator-browser/SKILL.md new file mode 100644 index 00000000000..74b97530799 --- /dev/null +++ b/.agents/skills/ios-simulator-browser/SKILL.md @@ -0,0 +1,51 @@ +--- +name: ios-simulator-browser +description: Stream an explicit iOS Simulator through pinned serve-sim into the T3 Code in-app browser or another agent browser. Use on Apple Silicon macOS when the user should watch simulator verification live or when browser-visible simulator evidence is needed. +--- + +# iOS Simulator Browser + +Use serve-sim as the shared visual feed for an iOS Simulator. Use `ios-debugger-agent` and XcodeBuildMCP semantic UI tools to drive the app; do not treat browser-canvas coordinates as a substitute for missing app accessibility. + +## Confirm availability + +serve-sim `0.1.45` requires Apple Silicon macOS, Xcode command-line tools, and Node.js 20 or newer. If the host is unsupported, continue with XcodeBuildMCP screenshots and report that live streaming was unavailable. + +When running inside T3 Code, use its product-native browser MCP to open the stream. Other agent hosts may use their own browser or preview surface. + +Keep serve-sim on its default `127.0.0.1` binding. Do not expose its preview to a LAN or tunnel unless the user explicitly requests that access and the network is trusted; the preview includes a token-gated shell-execution route. + +## Start one owned stream + +1. Obtain the exact simulator UDID from the iOS build or launch workflow. +2. Check whether an existing serve-sim stream for that UDID belongs to another task. Reuse it only when explicitly shared; never kill another task's stream. +3. Otherwise, clear only a stale stream for that UDID and start the pinned version with scoped cleanup: + + ```bash + SIMULATOR_ID= + cleanup_serve_sim() { + npx --yes serve-sim@0.1.45 --kill "$SIMULATOR_ID" >/dev/null 2>&1 || true + } + trap cleanup_serve_sim EXIT INT TERM HUP + cleanup_serve_sim + npx --yes serve-sim@0.1.45 "$SIMULATOR_ID" + ``` + +4. Keep the terminal alive and open the exact local URL printed by serve-sim in the agent's browser. +5. Verify that a live simulator frame renders. A loaded wrapper page is not sufficient evidence. + +## Observe while driving semantically + +- Let the user watch the serve-sim stream while XcodeBuildMCP performs `snapshot_ui`, semantic taps, typing, gestures, and screenshots. +- Keep the browser and Xcode tooling pinned to the same simulator UDID. +- Do not switch to generic desktop automation or browser-canvas clicking merely because the stream is visible. + +If the in-app browser explicitly reports that previews are unavailable, do not install unrelated browser automation. Continue through XcodeBuildMCP, capture a simulator screenshot, report the unavailable live stream, and clean up the owned serve-sim process. + +## Finish + +Stop the long-running terminal and wait for its cleanup trap to finish. If it disappeared without cleanup, run `npx --yes serve-sim@0.1.45 --kill ` for that exact simulator. Never run an unscoped `--kill`. + +## Upstream + +Adapted from OpenAI's [`build-ios-apps`](https://github.com/openai/plugins/tree/main/plugins/build-ios-apps) plugin version `0.1.2` (`ios-simulator-browser`, MIT). It invokes serve-sim `0.1.45` under its Apache-2.0 license without vendoring the package. diff --git a/.agents/skills/ios-simulator-browser/agents/openai.yaml b/.agents/skills/ios-simulator-browser/agents/openai.yaml new file mode 100644 index 00000000000..9e8cdf7a16b --- /dev/null +++ b/.agents/skills/ios-simulator-browser/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "iOS Simulator Browser" + short_description: "Stream iOS Simulator in the browser" + default_prompt: "Use $ios-simulator-browser to stream the selected iOS Simulator into the T3 Code in-app browser." diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md index c573807d615..c3dc1c103d4 100644 --- a/.agents/skills/test-t3-app/SKILL.md +++ b/.agents/skills/test-t3-app/SKILL.md @@ -5,6 +5,8 @@ description: Launch and test the T3 Code web app in isolated development environ # Test T3 App +Use this skill for the web client. For iOS Simulator, Android Emulator, or physical-device testing against an isolated T3 backend, use the sibling [`test-t3-mobile`](../test-t3-mobile/SKILL.md) skill. + ## Start an isolated web environment 1. Run commands from the repository root. diff --git a/.agents/skills/test-t3-mobile/SKILL.md b/.agents/skills/test-t3-mobile/SKILL.md new file mode 100644 index 00000000000..f3e3dcfd8ce --- /dev/null +++ b/.agents/skills/test-t3-mobile/SKILL.md @@ -0,0 +1,188 @@ +--- +name: test-t3-mobile +description: Launch and test T3 Code Mobile on an iOS Simulator or Android Emulator against disposable local T3 environments, including Metro and dev-client reuse, native rebuild decisions, per-client pairing, seeded projects, semantic UI control, screenshots, and iOS serve-sim streaming. Use after mobile UI or native changes, when reproducing phone or tablet behavior, pairing an emulator to isolated state, or verifying mobile behavior on macOS, Linux, or Windows. +--- + +# Test T3 Mobile + +Run one focused, end-to-end mobile verification pass against disposable T3 state. Use the sibling [`test-t3-app`](../test-t3-app/SKILL.md) skill as the detailed reference for pairing-token semantics and SQLite fixtures. + +Command examples use POSIX shell syntax. On Windows, use PowerShell equivalents: set variables with `$env:NAME = "value"`, use an explicit temporary directory from `[System.IO.Path]::GetTempPath()`, and run multiline examples on one line or with PowerShell backticks. Use `$env:ANDROID_HOME\platform-tools\adb.exe` when `adb` is not already on `PATH`. + +## Select a viable platform + +Inspect the host and the affected code before launching processes: + +- On macOS with Xcode, prefer one representative iOS Simulator when the change is cross-platform so the user can watch through serve-sim. Load and follow [`ios-debugger-agent`](../ios-debugger-agent/SKILL.md), and load [`ios-simulator-browser`](../ios-simulator-browser/SKILL.md) when live streaming is available. +- On macOS, Linux, or Windows with the Android SDK, use one Android Emulator when Android is the affected surface or iOS tooling is unavailable. +- When the change is platform-specific, test that platform. When neither platform is viable, report the missing SDK, emulator, or dev-client prerequisite rather than claiming verification. + +Do not treat unavailable iOS tooling as a blocker when Android is a valid representative target. + +## Choose the lightest valid launch path + +- For JavaScript, TypeScript, or asset-only changes, reuse a compatible installed development client and start Metro. Do not rebuild native code merely to load a new bundle. +- For native source, native dependencies, entitlements, config plugins, or generated project changes, rebuild the affected platform. +- Use `vp run ios:dev` or `vp run android:dev` only when an Expo clean prebuild is actually required; both commands regenerate the native project. +- If the user requested no native rebuild and no compatible app is installed, reuse an existing compatible `.app` or `.apk` artifact when available. Otherwise report the missing dev client instead of silently rebuilding. + +The development identity on both platforms is: + +- App: `T3 Code Dev` +- Bundle/package identifier: `com.t3tools.t3code.dev` +- URL scheme: `t3code-dev` + +Bundle or package presence proves the correct variant, not native compatibility. Reuse it only when the current changes did not alter its Expo SDK, native dependencies, config plugins, entitlements, generated project, or native source. + +## Start one disposable T3 environment + +Run backend commands from the repository root. Use the ignored, worktree-local `.t3` directory or create a fresh directory with the host OS's temporary-directory mechanism. An explicit base directory stores state in `/userdata`; never point testing at shared `~/.t3` state. + +Seed a small number of meaningful Git projects before starting the backend: + +```bash +node apps/server/src/bin.ts project add \ + --base-dir \ + --title +``` + +Running `project add` before the backend starts gives it exclusive offline database access. If a backend is already running, wait until it is ready so the CLI dispatches through the live server; never run offline mutations concurrently with the server. + +Use direct SQLite mutation only for disposable projection fixtures. Follow `test-t3-app` and stop the backend before writing. + +Start a headless backend after seeding: + +```bash +node apps/server/src/bin.ts serve \ + --host 127.0.0.1 \ + --port \ + --base-dir \ + --no-browser +``` + +Use these client origins: + +- iOS Simulator: `http://127.0.0.1:` +- Android Emulator: `http://10.0.2.2:` +- Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin + +Always enter the complete `http://` origin; the mobile host field otherwise assumes HTTPS. When testing web and mobile together, run `vp run dev --home-dir --host 127.0.0.1` instead and do not launch a second backend over the same base directory. + +## Start or reuse Metro safely + +Run Metro from `apps/mobile`. + +1. Inspect any process on the intended Metro port and its `/status` response. Reuse it only when it is healthy, belongs to this worktree, and matches `APP_VARIANT=development`, `--dev-client`, and scheme `t3code-dev`. +2. Never kill another worktree's Metro. Use a free explicit port when necessary. +3. Run `vp run dev:client` on the standard port. For another port, retain the complete development identity: + + ```bash + APP_VARIANT=development vp exec expo start \ + --dev-client \ + --scheme t3code-dev \ + --clear \ + --lan \ + --port + ``` + + In PowerShell, set `$env:APP_VARIANT = "development"` first and then run the `vp exec expo start ...` command without the leading assignment. + +4. Open the exact development-client URL for the selected device and confirm the loaded bundle belongs to this worktree and Metro port. + +### iOS launch + +Use `ios-debugger-agent` to select one UDID and set these XcodeBuildMCP session defaults: + +- Workspace: `/apps/mobile/ios/T3CodeDev.xcworkspace` +- Scheme: `T3CodeDev` +- Configuration: `Debug` +- Simulator ID: the selected UDID +- Bundle ID: `com.t3tools.t3code.dev` + +Check the installed client with: + +```bash +xcrun simctl get_app_container com.t3tools.t3code.dev app +xcrun simctl openurl +``` + +Accept the iOS confirmation prompt and dismiss the developer menu when it obscures the app. + +### Android launch + +Select one running emulator serial from `adb devices` and check the installed client: + +```bash +adb -s shell pm path com.t3tools.t3code.dev +adb -s reverse tcp: tcp: +adb -s shell am start -W \ + -a android.intent.action.VIEW \ + -d '' \ + com.t3tools.t3code.dev +``` + +Do not start, stop, erase, or reconfigure an emulator owned by another task. Track and later stop only processes owned by this test. + +## Pair each client once + +Issue a fresh credential against the running backend's exact base directory: + +```bash +T3CODE_PORT= node apps/server/src/bin.ts auth pairing create \ + --base-dir \ + --base-url \ + --ttl 15m \ + --label agent-mobile- +``` + +In PowerShell, set `$env:T3CODE_PORT = ""` first and run the `node ... auth pairing create` command without the leading assignment. + +If the visible Add Environment action is not exposed as a semantic target, open the app's registered route instead of guessing coordinates: + +```bash +xcrun simctl openurl 't3code-dev://connections/new' +adb -s shell am start -W \ + -a android.intent.action.VIEW \ + -d 't3code-dev://connections/new' \ + com.t3tools.t3code.dev +``` + +Run only the command for the selected platform. + +In T3 Code Dev, open Add Environment and enter the complete `` and newly printed `Token`. Verify the expected seeded projects appear before exercising the affected flow. + +Pairing credentials are secret, short-lived, and single-use. Create a different credential for every simulator, emulator, physical device, or browser. If an attempt fails, issue a new credential rather than retrying the old one. Do not expose tokens in screenshots, commits, or final responses. + +## Drive and observe the affected flow + +### iOS + +Use `snapshot_ui` and current element references from XcodeBuildMCP for taps and typing. Stream the same UDID through `ios-simulator-browser` so the user can watch in T3 Code when the host supports it. Use the stream as a visual feed rather than a reason to switch to fragile browser coordinates. + +### Android + +Prefer semantic Android automation exposed by the current agent host. Otherwise inspect the current hierarchy with `adb shell uiautomator dump`, target stable resource IDs, content descriptions, text, or bounds, and use scoped `adb shell input` actions. Refresh the hierarchy after navigation. Capture the final state with `adb exec-out screencap -p`. + +Android does not use serve-sim. Use a browser-compatible Android mirror when the host already provides one; otherwise return focused emulator screenshots as evidence rather than installing unrelated streaming infrastructure during verification. + +## Verify and clean up + +Exercise only the affected flow on one representative device unless the change specifically concerns platform, OS version, or screen size. Before finishing: + +1. Confirm the app connected to the intended disposable environment instead of merely rendering an empty disconnected state. +2. Capture the relevant final state. +3. Remove the disposable environment from T3 Code Dev. +4. Remove any `adb reverse` rule created for this test with `adb -s reverse --remove tcp:`. +5. Stop only the serve-sim, Metro, backend, emulator, and log processes started by this test. +6. Remove only base directories and temporary Git repositories deliberately created for this test. Preserve them when they contain useful reproduction evidence. + +Keep local verification focused. Do not turn this workflow into a full repository test run. + +## Troubleshoot predictable failures + +- **Old UI or an old error appears:** verify Metro's worktree, variant, URL, and port before diagnosing the app. +- **The environment remains empty:** verify the platform-specific HTTP origin, use a fresh token, and confirm project seeding used the identical base directory. +- **A second client cannot pair:** pairing tokens are single-use; issue another token. +- **iOS semantic actions fail:** set explicit XcodeBuildMCP defaults and refresh with `snapshot_ui`. +- **Android cannot reach Metro:** verify `adb reverse` for the exact Metro port and relaunch the development-client URL. +- **Android cannot reach the backend:** use `10.0.2.2`, not `127.0.0.1`, for the Android Emulator. diff --git a/.agents/skills/test-t3-mobile/agents/openai.yaml b/.agents/skills/test-t3-mobile/agents/openai.yaml new file mode 100644 index 00000000000..e9518ce9e04 --- /dev/null +++ b/.agents/skills/test-t3-mobile/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Test T3 Mobile" + short_description: "Test T3 Code on iOS or Android" + default_prompt: "Use $test-t3-mobile to launch T3 Code against an isolated backend and verify the affected flow on an available simulator or emulator." diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000000..a6369d240fc --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,9 @@ +[mcp_servers.xcodebuildmcp] +enabled = true +required = false +command = "npx" +args = ["--yes", "xcodebuildmcp@2.6.2", "mcp"] +startup_timeout_sec = 20.0 + +[mcp_servers.xcodebuildmcp.env] +XCODEBUILDMCP_ENABLED_WORKFLOWS = "simulator,ui-automation,debugging,logging" diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000000..213a64995fa --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "xcodebuildmcp": { + "command": "npx", + "args": ["--yes", "xcodebuildmcp@2.6.2", "mcp"], + "env": { + "XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,ui-automation,debugging,logging" + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 0a425cbc185..ef69591a340 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,10 @@ - Backend changes must include and run focused tests for the changed behavior. - Run targeted formatting, lint, and type checks for the affected scope when available. - Do not run repo-wide `vp check`, `vp run typecheck`, `vp run test`, or equivalent full-suite commands locally unless the user explicitly requests them. CI is responsible for the full verification suite. -- After frontend feature development or any user-visible frontend behavior change, the primary agent must use the `test-t3-app` skill once after integrating the work. Launch one isolated environment, authenticate through the printed pairing URL, and verify the affected flow in the controlled browser. - - Subagents must not independently launch dev servers or repeat integrated browser verification unless their delegated task explicitly requires it. +- After frontend feature development or any user-visible frontend behavior change, the primary agent must run one integrated verification pass for each affected client surface after integrating the work: + - Web: use the `test-t3-app` skill. Launch one isolated environment, authenticate through the printed pairing URL, and verify the affected flow in the controlled browser. + - Mobile: use the `test-t3-mobile` skill. Connect one representative iOS Simulator or Android Emulator available on the host to one isolated environment and verify the affected flow. On compatible macOS hosts, prefer iOS for cross-platform changes and stream it through serve-sim in the T3 Code in-app browser or another available agent browser; use Android when it is the affected or viable platform. + - Subagents must not independently launch dev servers or repeat integrated client verification unless their delegated task explicitly requires it. - Stop dev servers, watchers, and other long-running verification processes when the focused verification is complete. ## Package Roles diff --git a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx index d58ee338973..f88e3287445 100644 --- a/apps/mobile/src/features/connection/ConnectionSheetButton.tsx +++ b/apps/mobile/src/features/connection/ConnectionSheetButton.tsx @@ -58,6 +58,9 @@ export function ConnectionSheetButton(props: { return ( Date: Mon, 20 Jul 2026 01:33:01 +0200 Subject: [PATCH 13/39] fix(client): use lightweight connection probe (#4137) --- .../src/environment/ServerEnvironment.test.ts | 1 + .../src/environment/ServerEnvironment.ts | 1 + apps/server/src/ws.ts | 5 ++ .../src/connection/supervisor.test.ts | 2 +- .../client-runtime/src/rpc/session.test.ts | 80 ++++++++++++++++++- packages/client-runtime/src/rpc/session.ts | 14 +++- packages/contracts/src/environment.ts | 1 + packages/contracts/src/rpc.ts | 8 ++ 8 files changed, 105 insertions(+), 7 deletions(-) diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index 6b3290246fe..61892d53d63 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -67,6 +67,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { expect(first.environmentId).toBe(second.environmentId); expect(second.capabilities.repositoryIdentity).toBe(true); + expect(second.capabilities.connectionProbe).toBe(true); }), ); diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index b5fbd8e1088..1c0d34ea5bc 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -135,6 +135,7 @@ export const make = Effect.gen(function* () { serverVersion: packageJson.version, capabilities: { repositoryIdentity: true, + connectionProbe: true, }, }; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 32dcb8a13d6..c7223d09dc5 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -284,6 +284,7 @@ const RPC_REQUIRED_SCOPE = new Map([ [ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope], [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope], [ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope], + [WS_METHODS.serverProbe, AuthOrchestrationReadScope], [WS_METHODS.serverGetConfig, AuthOrchestrationReadScope], [WS_METHODS.serverRefreshProviders, AuthOrchestrationOperateScope], [WS_METHODS.serverUpdateProvider, AuthOrchestrationOperateScope], @@ -1238,6 +1239,10 @@ const makeWsRpcLayer = ( }), { "rpc.aggregate": "orchestration" }, ), + [WS_METHODS.serverProbe]: (_input) => + observeRpcEffect(WS_METHODS.serverProbe, Effect.succeed({}), { + "rpc.aggregate": "server", + }), [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server", diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index eadeceacc2c..f3901e42251 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -652,7 +652,7 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect("keeps a healthy session when the application becomes active", () => + it.effect("probes the active session without reconnecting on application activation", () => Effect.gen(function* () { const probeCount = yield* Ref.make(0); const probeCalled = yield* Deferred.make(); diff --git a/packages/client-runtime/src/rpc/session.test.ts b/packages/client-runtime/src/rpc/session.test.ts index 7820c93a935..402faf4f0e2 100644 --- a/packages/client-runtime/src/rpc/session.test.ts +++ b/packages/client-runtime/src/rpc/session.test.ts @@ -109,6 +109,7 @@ const SERVER_CONFIG: ServerConfigType = { serverVersion: "0.0.0-test", capabilities: { repositoryIdentity: true, + connectionProbe: true, }, }, auth: { @@ -141,6 +142,16 @@ const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString); const decodeRpcRequest = Schema.decodeUnknownSync(RpcRequest); const encodeJson = Schema.encodeUnknownSync(Schema.UnknownFromJsonString); const encodeServerConfig = Schema.encodeSync(ServerConfig); +const ENCODED_SERVER_CONFIG = encodeServerConfig(SERVER_CONFIG); +const LEGACY_SERVER_CONFIG = { + ...ENCODED_SERVER_CONFIG, + environment: { + ...ENCODED_SERVER_CONFIG.environment, + capabilities: { + repositoryIdentity: true, + }, + }, +}; const makeFactory = Effect.fn("TestRpcSessionFactory.make")(function* () { const sockets: TestWebSocket[] = []; @@ -169,9 +180,10 @@ const awaitSocket = Effect.fn("TestRpcSessionFactory.awaitSocket")(function* ( const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( socket: TestWebSocket, + index = 0, ) { for (let attempt = 0; attempt < 100; attempt += 1) { - const request = socket.sent[0]; + const request = socket.sent[index]; if (request) { return decodeRpcRequest(decodeJson(request)); } @@ -182,6 +194,7 @@ const awaitRequest = Effect.fn("TestRpcSessionFactory.awaitRequest")(function* ( const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialConfig")(function* ( socket: TestWebSocket, + config: unknown = ENCODED_SERVER_CONFIG, ) { const request = yield* awaitRequest(socket); expect(request).toMatchObject({ @@ -195,7 +208,7 @@ const completeInitialConfig = Effect.fn("TestRpcSessionFactory.completeInitialCo requestId: request.id, exit: { _tag: "Success", - value: encodeServerConfig(SERVER_CONFIG), + value: config, }, }), ); @@ -218,6 +231,30 @@ describe("RpcSessionFactory", () => { expect(config).toEqual(SERVER_CONFIG); expect(socket.sent).toHaveLength(1); + const probeFiber = yield* Effect.forkChild(session.probe); + const probeRequest = yield* awaitRequest(socket, 1); + expect(probeRequest).toMatchObject({ + _tag: "Request", + tag: WS_METHODS.serverProbe, + payload: {}, + }); + socket.serverMessage( + encodeJson({ + _tag: "Exit", + requestId: probeRequest.id, + exit: { + _tag: "Success", + value: {}, + }, + }), + ); + yield* Fiber.join(probeFiber); + + expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ + WS_METHODS.serverGetConfig, + WS_METHODS.serverProbe, + ]); + socket.close(1012, "service restart"); const error = yield* Effect.flip(session.closed); @@ -250,6 +287,45 @@ describe("RpcSessionFactory", () => { }), ); + it.effect("uses the legacy config RPC for probes when the server lacks the capability", () => + Effect.scoped( + Effect.gen(function* () { + const { factory, sockets } = yield* makeFactory(); + const session = yield* factory.connect(PREPARED); + const readyFiber = yield* Effect.forkChild(session.ready); + const socket = yield* awaitSocket(sockets); + + socket.open(); + yield* completeInitialConfig(socket, LEGACY_SERVER_CONFIG); + yield* Fiber.join(readyFiber); + + const probeFiber = yield* Effect.forkChild(session.probe); + const probeRequest = yield* awaitRequest(socket, 1); + expect(probeRequest).toMatchObject({ + _tag: "Request", + tag: WS_METHODS.serverGetConfig, + payload: {}, + }); + socket.serverMessage( + encodeJson({ + _tag: "Exit", + requestId: probeRequest.id, + exit: { + _tag: "Success", + value: LEGACY_SERVER_CONFIG, + }, + }), + ); + yield* Fiber.join(probeFiber); + + expect(socket.sent.map((request) => decodeRpcRequest(decodeJson(request)).tag)).toEqual([ + WS_METHODS.serverGetConfig, + WS_METHODS.serverGetConfig, + ]); + }), + ), + ); + it.effect("fails readiness when the websocket never opens", () => Effect.gen(function* () { const { factory, sockets } = yield* makeFactory(); diff --git a/packages/client-runtime/src/rpc/session.ts b/packages/client-runtime/src/rpc/session.ts index f9594c1b7ca..9625effa406 100644 --- a/packages/client-runtime/src/rpc/session.ts +++ b/packages/client-runtime/src/rpc/session.ts @@ -42,8 +42,9 @@ export class RpcSessionFactory extends Context.Service< type InitialConfigError = Effect.Error< ReturnType >; +type ProbeError = Effect.Error>; -function mapInitialConfigError(error: InitialConfigError): ConnectionAttemptError { +function mapSessionRpcError(error: InitialConfigError | ProbeError): ConnectionAttemptError { switch (error._tag) { case "EnvironmentAuthorizationError": return new ConnectionBlockedError({ @@ -115,12 +116,17 @@ export const make = Effect.gen(function* () { const client = yield* makeWsRpcProtocolClient.pipe(Effect.provide(protocolContext)); const initialConfig = yield* Effect.cached( client[WS_METHODS.serverGetConfig]({}).pipe( - Effect.mapError(mapInitialConfigError), + Effect.mapError(mapSessionRpcError), Effect.withSpan("environment.initialSync"), ), ); - const probe = client[WS_METHODS.serverGetConfig]({}).pipe( - Effect.mapError(mapInitialConfigError), + const probe = initialConfig.pipe( + Effect.flatMap((config) => + (config.environment.capabilities.connectionProbe === true + ? client[WS_METHODS.serverProbe]({}) + : client[WS_METHODS.serverGetConfig]({}) + ).pipe(Effect.mapError(mapSessionRpcError)), + ), Effect.asVoid, Effect.withSpan("clientRuntime.connection.rpcSession.probe"), ); diff --git a/packages/contracts/src/environment.ts b/packages/contracts/src/environment.ts index fb52972d5aa..bc8db25a95a 100644 --- a/packages/contracts/src/environment.ts +++ b/packages/contracts/src/environment.ts @@ -22,6 +22,7 @@ export type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.T export const ExecutionEnvironmentCapabilities = Schema.Struct({ repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + connectionProbe: Schema.optionalKey(Schema.Boolean), }); export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 48c5d9a774d..0356aa1807b 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -201,6 +201,7 @@ export const WS_METHODS = { previewAutomationFocusHost: "previewAutomation.focusHost", // Server meta + serverProbe: "server.probe", serverGetConfig: "server.getConfig", serverRefreshProviders: "server.refreshProviders", serverUpdateProvider: "server.updateProvider", @@ -246,6 +247,12 @@ export const WsServerRemoveKeybindingRpc = Rpc.make(WS_METHODS.serverRemoveKeybi error: Schema.Union([KeybindingsConfigError, EnvironmentAuthorizationError]), }); +export const WsServerProbeRpc = Rpc.make(WS_METHODS.serverProbe, { + payload: Schema.Struct({}), + success: Schema.Struct({}), + error: EnvironmentAuthorizationError, +}); + export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, { payload: Schema.Struct({}), success: ServerConfig, @@ -682,6 +689,7 @@ export const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, }); export const WsRpcGroup = RpcGroup.make( + WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, From 3795dbb01723d7474db8ddf805b1f64d5b56919c Mon Sep 17 00:00:00 2001 From: David Whatley Date: Mon, 20 Jul 2026 03:31:13 -0500 Subject: [PATCH 14/39] fix(server): resolve Claude SDK executable path on Windows npm installs (#3740) --- .../provider/Drivers/ClaudeExecutable.test.ts | 124 ++++++++++++++++++ .../src/provider/Drivers/ClaudeExecutable.ts | 90 +++++++++++++ .../src/provider/Layers/ClaudeAdapter.ts | 7 +- .../src/provider/Layers/ClaudeProvider.ts | 7 +- 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/provider/Drivers/ClaudeExecutable.test.ts create mode 100644 apps/server/src/provider/Drivers/ClaudeExecutable.ts diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts new file mode 100644 index 00000000000..020fc48a465 --- /dev/null +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from "@effect/vitest"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { SpawnExecutableResolution } from "@t3tools/shared/shell"; +import * as Effect from "effect/Effect"; + +import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts"; + +const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm"; +const NPM_SHIM = `${NPM_DIR}\\claude.cmd`; +const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; +const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`; + +function withWindowsResolution(input: { + readonly resolvedCommand: string | undefined; + readonly existingFiles?: ReadonlyArray; +}) { + const existing = new Set(input.existingFiles ?? []); + return (effect: Effect.Effect) => + effect.pipe( + Effect.provideService(HostProcessPlatform, "win32"), + Effect.provideService(SpawnExecutableResolution, () => input.resolvedCommand), + Effect.provideService(ClaudeExecutableFileCheck, (filePath) => existing.has(filePath)), + ); +} + +describe("resolveClaudeSdkExecutablePath", () => { + it.effect("returns the configured path unchanged on non-Windows platforms", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + Effect.provideService(HostProcessPlatform, "darwin"), + Effect.provideService(SpawnExecutableResolution, () => { + throw new Error("must not resolve on non-Windows platforms"); + }), + ), + ).toBe("claude"); + }), + ); + + it.effect("returns the resolved absolute path for native Windows executables", () => + Effect.gen(function* () { + const nativeBinary = "C:\\Users\\dev\\.local\\bin\\claude.exe"; + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ resolvedCommand: nativeBinary }), + ), + ).toBe(nativeBinary); + }), + ); + + it.effect("follows an npm launcher shim to the packaged native binary", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ + resolvedCommand: NPM_SHIM, + existingFiles: [NPM_PACKAGE_EXE, NPM_PACKAGE_CLI], + }), + ), + ).toBe(NPM_PACKAGE_EXE); + }), + ); + + it.effect("follows .bat and .ps1 launcher shims the same way", () => + Effect.gen(function* () { + for (const shim of [`${NPM_DIR}\\claude.bat`, `${NPM_DIR}\\claude.ps1`]) { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ + resolvedCommand: shim, + existingFiles: [NPM_PACKAGE_EXE], + }), + ), + ).toBe(NPM_PACKAGE_EXE); + } + }), + ); + + it.effect("normalizes mixed-case shim extensions before matching", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ + resolvedCommand: `${NPM_DIR}\\claude.CMD`, + existingFiles: [NPM_PACKAGE_EXE], + }), + ), + ).toBe(NPM_PACKAGE_EXE); + }), + ); + + it.effect("falls back to cli.js when the package ships no native binary", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ + resolvedCommand: NPM_SHIM, + existingFiles: [NPM_PACKAGE_CLI], + }), + ), + ).toBe(NPM_PACKAGE_CLI); + }), + ); + + it.effect("returns the configured path when a shim has no known package entry", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ resolvedCommand: NPM_SHIM }), + ), + ).toBe("claude"); + }), + ); + + it.effect("returns the configured path when command resolution finds nothing", () => + Effect.gen(function* () { + expect( + yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( + withWindowsResolution({ resolvedCommand: undefined }), + ), + ).toBe("claude"); + }), + ); +}); diff --git a/apps/server/src/provider/Drivers/ClaudeExecutable.ts b/apps/server/src/provider/Drivers/ClaudeExecutable.ts new file mode 100644 index 00000000000..febfdb26f9e --- /dev/null +++ b/apps/server/src/provider/Drivers/ClaudeExecutable.ts @@ -0,0 +1,90 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; + +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { SpawnExecutableResolution } from "@t3tools/shared/shell"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; + +/** + * Windows launcher-script extensions that Node cannot spawn without a shell + * (`spawn EINVAL` since Node 20.12) and that the Claude Agent SDK therefore + * cannot use as `pathToClaudeCodeExecutable`. + */ +const WINDOWS_SHIM_EXTENSIONS: ReadonlySet = new Set([".cmd", ".bat", ".ps1"]); + +/** + * Entry points of the npm `@anthropic-ai/claude-code` package relative to the + * global `node_modules` directory that sits next to the npm launcher shim. + * Newer package versions ship a native `bin/claude.exe`; older versions only + * ship `cli.js`, which the SDK runs with a JavaScript runtime. + */ +const NPM_PACKAGE_ENTRY_CANDIDATES = [ + ["node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"], + ["node_modules", "@anthropic-ai", "claude-code", "cli.js"], +] as const; + +export type ExecutableFileCheck = (filePath: string) => boolean; + +function isExistingFile(filePath: string): boolean { + try { + return NodeFS.statSync(filePath).isFile(); + } catch { + return false; + } +} + +/** Injectable file-existence check so tests can run against a fake filesystem. */ +export const ClaudeExecutableFileCheck = Context.Reference( + "server/provider/Drivers/ClaudeExecutableFileCheck", + { + defaultValue: () => isExistingFile, + }, +); + +/** + * Resolves the configured Claude binary path into a value the Claude Agent + * SDK can spawn directly via `pathToClaudeCodeExecutable`. + * + * The SDK spawns the given path without a shell and without Windows PATH / + * PATHEXT resolution, so a bare command name like `claude` fails with + * "native binary not found" and an npm `claude.cmd` shim fails with + * `spawn EINVAL`. CLI probes avoid this via `resolveSpawnCommand`, which can + * fall back to `shell: true`; the SDK offers no such escape hatch. + * + * On Windows this resolves the command against PATH/PATHEXT and, when the + * result is an npm launcher shim, follows it to the real package entry + * (`bin/claude.exe`, or `cli.js` for older package versions). On other + * platforms the configured value is returned unchanged. + */ +export const resolveClaudeSdkExecutablePath = Effect.fn("resolveClaudeSdkExecutablePath")( + function* (binaryPath: string, environment: NodeJS.ProcessEnv): Effect.fn.Return { + const platform = yield* HostProcessPlatform; + if (platform !== "win32") { + return binaryPath; + } + + const resolveExecutable = yield* SpawnExecutableResolution; + const isFile = yield* ClaudeExecutableFileCheck; + const resolved = resolveExecutable(binaryPath, platform, environment) ?? binaryPath; + const extension = NodePath.win32.extname(resolved).toLowerCase(); + if (!WINDOWS_SHIM_EXTENSIONS.has(extension)) { + return resolved; + } + + const shimDirectory = NodePath.win32.dirname(resolved); + for (const entrySegments of NPM_PACKAGE_ENTRY_CANDIDATES) { + const candidate = NodePath.win32.join(shimDirectory, ...entrySegments); + if (isFile(candidate)) { + return candidate; + } + } + + yield* Effect.logWarning( + "Claude launcher shim resolved but no known package entry was found next to it; the Claude Agent SDK cannot spawn launcher scripts directly.", + { binaryPath, resolvedShimPath: resolved }, + ); + return binaryPath; + }, +); diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 97a93f85829..f6e63eeffad 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -70,6 +70,7 @@ import * as Stream from "effect/Stream"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; +import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; import { getClaudeModelCapabilities, @@ -1347,6 +1348,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( Effect.provideService(Path.Path, path), ); + const claudeSdkExecutablePath = yield* resolveClaudeSdkExecutablePath( + claudeSettings.binaryPath, + claudeEnvironment, + ); const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined @@ -3405,7 +3410,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => runPromise(canUseToolEffect(toolName, toolInput, callbackOptions)); - const claudeBinaryPath = claudeSettings.binaryPath; + const claudeBinaryPath = claudeSdkExecutablePath; const extraArgs = parseCliArgs(claudeSettings.launchArgs).flags; const modelSelection = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index af8f5d6704b..d49b4770a09 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -37,6 +37,7 @@ import { spawnAndCollect, type ServerProviderDraft, } from "../providerSnapshot.ts"; +import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ @@ -588,6 +589,10 @@ const probeClaudeCapabilities = ( const abort = new AbortController(); return Effect.gen(function* () { const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, environment); + const executablePath = yield* resolveClaudeSdkExecutablePath( + claudeSettings.binaryPath, + claudeEnvironment, + ); return yield* Effect.tryPromise(async () => { const q = claudeQuery({ // Never yield — we only need initialization data, not a conversation. @@ -598,7 +603,7 @@ const probeClaudeCapabilities = ( })(), options: { persistSession: false, - pathToClaudeCodeExecutable: claudeSettings.binaryPath, + pathToClaudeCodeExecutable: executablePath, abortController: abort, settingSources: ["user", "project", "local"], allowedTools: [], From 63b6b44627d605afba7ec2549f817c23c0f903ad Mon Sep 17 00:00:00 2001 From: coach007 <6238600+keeperxy@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:32:14 +0200 Subject: [PATCH 15/39] Fix project action preview settings persistence (#3842) --- apps/web/src/components/ChatView.tsx | 17 ++---------- apps/web/src/projectScripts.test.ts | 41 ++++++++++++++++++++++++++++ apps/web/src/projectScripts.ts | 25 +++++++++++++++++ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f175a21a131..c296c717066 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -145,6 +145,7 @@ import { stackedThreadToast, toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; import { type NewProjectScriptInput } from "./ProjectScriptsControl"; import { + buildProjectScript, commandForProjectScript, nextProjectScriptId, projectScriptIdFromCommand, @@ -2752,13 +2753,7 @@ function ChatViewContent(props: ChatViewProps) { input.name, activeProject.scripts.map((script) => script.id), ); - const nextScript: ProjectScript = { - id: nextId, - name: input.name, - command: input.command, - icon: input.icon, - runOnWorktreeCreate: input.runOnWorktreeCreate, - }; + const nextScript = buildProjectScript(nextId, input); const nextScripts = input.runOnWorktreeCreate ? [ ...activeProject.scripts.map((script) => @@ -2792,13 +2787,7 @@ function ChatViewContent(props: ChatViewProps) { return AsyncResult.failure(Cause.fail(new Error("Script not found."))); } - const updatedScript: ProjectScript = { - ...existingScript, - name: input.name, - command: input.command, - icon: input.icon, - runOnWorktreeCreate: input.runOnWorktreeCreate, - }; + const updatedScript = buildProjectScript(existingScript.id, input); const nextScripts = activeProject.scripts.map((script) => script.id === scriptId ? updatedScript diff --git a/apps/web/src/projectScripts.test.ts b/apps/web/src/projectScripts.test.ts index 3597b714c77..1f7a6bfaa9f 100644 --- a/apps/web/src/projectScripts.test.ts +++ b/apps/web/src/projectScripts.test.ts @@ -6,6 +6,7 @@ import { } from "@t3tools/shared/projectScripts"; import { + buildProjectScript, commandForProjectScript, nextProjectScriptId, primaryProjectScript, @@ -13,6 +14,46 @@ import { } from "./projectScripts"; describe("projectScripts helpers", () => { + it("builds scripts with preview settings", () => { + expect( + buildProjectScript("dev", { + name: "Dev server", + command: "pnpm dev", + icon: "debug", + runOnWorktreeCreate: false, + previewUrl: "http://localhost:5733", + autoOpenPreview: true, + }), + ).toEqual({ + id: "dev", + name: "Dev server", + command: "pnpm dev", + icon: "debug", + runOnWorktreeCreate: false, + previewUrl: "http://localhost:5733", + autoOpenPreview: true, + }); + }); + + it("omits preview settings when no preview URL is configured", () => { + expect( + buildProjectScript("test", { + name: "Test", + command: "pnpm test", + icon: "test", + runOnWorktreeCreate: false, + previewUrl: null, + autoOpenPreview: false, + }), + ).toEqual({ + id: "test", + name: "Test", + command: "pnpm test", + icon: "test", + runOnWorktreeCreate: false, + }); + }); + it("builds and parses script run commands", () => { const command = commandForProjectScript("lint"); expect(command).toBe("script.lint.run"); diff --git a/apps/web/src/projectScripts.ts b/apps/web/src/projectScripts.ts index f0aeead1411..e3efc9e3a33 100644 --- a/apps/web/src/projectScripts.ts +++ b/apps/web/src/projectScripts.ts @@ -7,6 +7,31 @@ import { import * as Schema from "effect/Schema"; const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN); +export interface ProjectScriptInput { + readonly name: ProjectScript["name"]; + readonly command: ProjectScript["command"]; + readonly icon: ProjectScript["icon"]; + readonly runOnWorktreeCreate: ProjectScript["runOnWorktreeCreate"]; + readonly previewUrl: Exclude | null; + readonly autoOpenPreview: boolean; +} + +export function buildProjectScript(id: string, input: ProjectScriptInput): ProjectScript { + return { + id, + name: input.name, + command: input.command, + icon: input.icon, + runOnWorktreeCreate: input.runOnWorktreeCreate, + ...(input.previewUrl === null + ? {} + : { + previewUrl: input.previewUrl, + autoOpenPreview: input.autoOpenPreview, + }), + }; +} + function normalizeScriptId(value: string): string { const cleaned = value .trim() From 78485e6d50d06180618aacd0d25151e831f666a4 Mon Sep 17 00:00:00 2001 From: Carlos Rico-Ospina Date: Mon, 20 Jul 2026 04:32:44 -0400 Subject: [PATCH 16/39] fix(desktop): allow clipboard writes in the preview browser (#3889) --- .../src/preview/BrowserSession.test.ts | 51 +++++++++++++++++++ apps/desktop/src/preview/BrowserSession.ts | 20 +++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/preview/BrowserSession.test.ts b/apps/desktop/src/preview/BrowserSession.test.ts index e258bb2dfc5..743fd6a1fce 100644 --- a/apps/desktop/src/preview/BrowserSession.test.ts +++ b/apps/desktop/src/preview/BrowserSession.test.ts @@ -15,6 +15,7 @@ const { fromPartition, sessions } = vi.hoisted(() => ({ readonly clearStorageData: ReturnType; readonly getUserAgent: ReturnType; readonly setPermissionRequestHandler: ReturnType; + readonly setPermissionCheckHandler: ReturnType; readonly setUserAgent: ReturnType; } >(), @@ -40,6 +41,7 @@ describe("BrowserSession", () => { clearStorageData: vi.fn(() => Promise.resolve()), getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/0.0.27"), setPermissionRequestHandler: vi.fn(), + setPermissionCheckHandler: vi.fn(), setUserAgent: vi.fn(), }; sessions.set(partition, browserSession); @@ -61,6 +63,55 @@ describe("BrowserSession", () => { }).pipe(Effect.provide(layer)), ); + it.effect("grants clipboard-sanitized-write through both the request and check handlers", () => + Effect.gen(function* () { + const browserSessions = yield* BrowserSession.BrowserSession; + const partition = yield* browserSessions.getPartition("scope-a"); + yield* browserSessions.getSession("scope-a"); + + const browserSession = sessions.get(partition); + assert.isDefined(browserSession); + + const requestHandler = browserSession.setPermissionRequestHandler.mock.calls[0]?.[0]; + const checkHandler = browserSession.setPermissionCheckHandler.mock.calls[0]?.[0]; + assert.isFunction(requestHandler); + assert.isFunction(checkHandler); + + const requestAllows = (permission: string): boolean => { + let granted: boolean | undefined; + requestHandler(null, permission, (value: boolean) => { + granted = value; + }); + assert.isDefined(granted); + return granted; + }; + + for (const permission of [ + "clipboard-read", + "clipboard-sanitized-write", + "notifications", + "geolocation", + ]) { + assert.isTrue(requestAllows(permission), `request handler should allow ${permission}`); + assert.isTrue( + checkHandler(null, permission) as boolean, + `check handler should allow ${permission}`, + ); + } + + // `clipboard-write` is not a real Electron permission — the async write API + // uses `clipboard-sanitized-write` — so the stale name must not be granted, + // and unrelated permissions stay denied. + for (const permission of ["clipboard-write", "midi"]) { + assert.isFalse(requestAllows(permission), `request handler should deny ${permission}`); + assert.isFalse( + checkHandler(null, permission) as boolean, + `check handler should deny ${permission}`, + ); + } + }).pipe(Effect.provide(layer)), + ); + it.effect("preserves partition scope and the platform failure chain", () => { const nativeCause = new Error("native digest failed"); const platformCause = PlatformError.systemError({ diff --git a/apps/desktop/src/preview/BrowserSession.ts b/apps/desktop/src/preview/BrowserSession.ts index afa8dafe976..aa0b0743e93 100644 --- a/apps/desktop/src/preview/BrowserSession.ts +++ b/apps/desktop/src/preview/BrowserSession.ts @@ -11,6 +11,20 @@ import * as SynchronizedRef from "effect/SynchronizedRef"; const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; +// Permissions granted to preview web content. `clipboard-sanitized-write` is the +// Electron permission behind `navigator.clipboard.writeText()` — note it is NOT +// `clipboard-write`, which is not a valid Electron permission name. Async +// clipboard writes are gated by the permission *check* handler (not only the +// request handler), so both handlers must allow it; otherwise built-in "Copy" +// buttons — e.g. the Next.js / Vercel error overlay — fail with +// `Failed to execute 'writeText' on 'Clipboard': Write permission denied`. +const ALLOWED_PREVIEW_PERMISSIONS: ReadonlySet = new Set([ + "clipboard-read", + "clipboard-sanitized-write", + "notifications", + "geolocation", +]); + export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass()( "BrowserSessionPartitionDerivationError", { @@ -120,9 +134,11 @@ export const make = Effect.gen(function* BrowserSessionMake() { .replace(/\s*t3code\/[\d.]+/, ""); browserSession.setUserAgent(userAgent); browserSession.setPermissionRequestHandler((_webContents, permission, callback) => { - const allowed = ["clipboard-read", "clipboard-write", "notifications", "geolocation"]; - callback(allowed.includes(permission)); + callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission)); }); + browserSession.setPermissionCheckHandler((_webContents, permission) => + ALLOWED_PREVIEW_PERMISSIONS.has(permission), + ); const next = new Map(sessions); next.set(partition, browserSession); return [browserSession, next] as const; From dfbda843628c817a48523b1a4a28a6ef232d1d65 Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Mon, 20 Jul 2026 04:33:43 -0400 Subject: [PATCH 17/39] fix(web): handle sidebar shortcut before editors (#3921) --- apps/web/src/components/AppSidebarLayout.tsx | 11 +++++++++-- .../src/components/settings/KeybindingsSettings.tsx | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 587ec65cbc9..4d0486aa866 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -33,6 +33,12 @@ function SidebarControl() { useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; + if ( + event.target instanceof HTMLElement && + event.target.closest("[data-keybinding-capture]") + ) { + return; + } if (resolveShortcutCommand(event, keybindings) !== "sidebar.toggle") return; event.preventDefault(); @@ -40,8 +46,9 @@ function SidebarControl() { toggleSidebar(); }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); + // Capture before focused editors consume commands such as Mod+B for rich-text formatting. + window.addEventListener("keydown", onKeyDown, true); + return () => window.removeEventListener("keydown", onKeyDown, true); }, [keybindings, toggleSidebar]); return ( diff --git a/apps/web/src/components/settings/KeybindingsSettings.tsx b/apps/web/src/components/settings/KeybindingsSettings.tsx index b7dbbd3575b..67c33c9b942 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.tsx +++ b/apps/web/src/components/settings/KeybindingsSettings.tsx @@ -846,6 +846,7 @@ function KeybindingTableRow({ ) : (
Date: Mon, 20 Jul 2026 10:35:08 +0200 Subject: [PATCH 18/39] fix(server): recognize Bedrock-backed Claude as authenticated (#3931) --- .../src/provider/Layers/ClaudeProvider.ts | 29 +++++++++++++++---- .../provider/Layers/ProviderRegistry.test.ts | 26 +++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index d49b4770a09..94b24405eee 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -480,9 +480,19 @@ function claudeAuthMetadata(input: { return undefined; } +function apiProviderAuthMetadata( + apiProvider: string | undefined, +): { readonly type: string; readonly label: string } | undefined { + return apiProvider === "bedrock" ? { type: "bedrock", label: "Amazon Bedrock" } : undefined; +} + // ── SDK capability probe ──────────────────────────────────────────── -const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000; +// Amazon Bedrock initializes far slower than first-party auth: the SDK boots the +// Bedrock backend and runs the `awsAuthRefresh` credential hook before returning +// account info. The previous 8s budget expired mid-init, so the probe returned +// `undefined` and left the provider unverified and unselectable in the picker. +const CAPABILITIES_PROBE_TIMEOUT_MS = 25_000; function nonEmptyProbeString(value: string): string | undefined { const candidate = value.trim(); @@ -493,6 +503,12 @@ type ClaudeCapabilitiesProbe = { readonly email: string | undefined; readonly subscriptionType: string | undefined; readonly tokenSource: string | undefined; + /** + * Active API backend reported by the SDK's `AccountInfo`. Anthropic OAuth + * login only applies when `"firstParty"`; for Amazon Bedrock (`"bedrock"`) + * the subscription/token fields are absent and auth is external AWS creds. + */ + readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray; }; @@ -618,12 +634,14 @@ const probeClaudeCapabilities = ( readonly email?: string; readonly subscriptionType?: string; readonly tokenSource?: string; + readonly apiProvider?: string; } | undefined; return { email: account?.email, subscriptionType: account?.subscriptionType, tokenSource: account?.tokenSource, + apiProvider: account?.apiProvider, slashCommands: parseClaudeInitializationCommands(init.commands), } satisfies ClaudeCapabilitiesProbe; }); @@ -799,10 +817,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( }); } - const authMetadata = claudeAuthMetadata({ - subscriptionType: capabilities.subscriptionType, - authMethod: capabilities.tokenSource, - }); + const authMetadata = + claudeAuthMetadata({ + subscriptionType: capabilities.subscriptionType, + authMethod: capabilities.tokenSource, + }) ?? apiProviderAuthMetadata(capabilities.apiProvider); return buildServerProvider({ presentation: CLAUDE_PRESENTATION, enabled: claudeSettings.enabled, diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 5456a90bdf5..78ff5cf493d 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -100,6 +100,7 @@ type TestClaudeCapabilities = { readonly email: string | undefined; readonly subscriptionType: string | undefined; readonly tokenSource: string | undefined; + readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray; }; @@ -109,6 +110,7 @@ function claudeCapabilities(overrides: Partial = {}) { email: undefined, subscriptionType: undefined, tokenSource: undefined, + apiProvider: undefined, slashCommands: [], ...overrides, }); @@ -1492,6 +1494,30 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ), ); + it.effect("returns ready and labels Bedrock-backed Claude as authenticated", () => + Effect.gen(function* () { + // Bedrock authenticates via external AWS credentials, so the SDK init + // reports only `apiProvider` with no subscription or token. + const status = yield* checkClaudeProviderStatus( + defaultClaudeSettings, + claudeCapabilities({ apiProvider: "bedrock" }), + ); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.installed, true); + assert.strictEqual(status.auth.status, "authenticated"); + assert.strictEqual(status.auth.type, "bedrock"); + assert.strictEqual(status.auth.label, "Amazon Bedrock"); + }).pipe( + Effect.provide( + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Unexpected args: ${joined}`); + }), + ), + ), + ); + it.effect("includes Claude Fable 5 on supported Claude Code versions", () => Effect.gen(function* () { const status = yield* checkClaudeProviderStatus( From d7baa37e5c25a1335a117ee3c9f5b5b0d5bbd832 Mon Sep 17 00:00:00 2001 From: mel Date: Mon, 20 Jul 2026 10:35:47 +0200 Subject: [PATCH 19/39] =?UTF-8?q?Fix=20incorrect=20pluralization=20of=20?= =?UTF-8?q?=E2=80=9Centry=E2=80=9D=20(#3933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/components/chat/MessagesTimeline.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index d67dd286faa..46d6a4050b8 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1157,7 +1157,13 @@ function WorkGroupToggleTimelineRow({ row: Extract; }) { const ctx = use(TimelineRowCtx); - const labelNoun = row.onlyToolEntries ? "tool call" : "log entry"; + const labelNoun = row.onlyToolEntries + ? row.hiddenCount === 1 + ? "tool call" + : "tool calls" + : row.hiddenCount === 1 + ? "log entry" + : "log entries"; return ( From 749baec353d5675f1acac81f22aa15da573e71c7 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 20 Jul 2026 01:42:02 -0700 Subject: [PATCH 20/39] feat(server): title background-task work-log rows with the task name (#3751) Co-authored-by: Claude Fable 5 --- .../Layers/ProviderRuntimeIngestion.test.ts | 190 ++++++++++++++++++ .../Layers/ProviderRuntimeIngestion.ts | 102 +++++++++- 2 files changed, 289 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 001ba388949..72976768fec 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -2947,6 +2947,196 @@ describe("ProviderRuntimeIngestion", () => { ).toBe("# Plan title"); }); + it("titles task activities with the task description, including on completion", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + + harness.emit({ + type: "task.started", + eventId: asEventId("evt-named-task-started"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-named-task"), + payload: { + taskId: "named-task-1", + description: "Typecheck mobile app", + taskType: "local_bash", + }, + }); + + harness.emit({ + type: "task.progress", + eventId: asEventId("evt-named-task-progress"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-named-task"), + payload: { + taskId: "named-task-1", + description: "Typecheck mobile app", + summary: "Running tsc across the mobile workspace.", + }, + }); + + harness.emit({ + type: "task.completed", + eventId: asEventId("evt-named-task-completed"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-named-task"), + payload: { + taskId: "named-task-1", + status: "completed", + summary: "Typecheck finished without errors.", + }, + }); + + const thread = await waitForThread(harness.readModel, (entry) => + entry.activities.some( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed", + ), + ); + + const progress = thread.activities.find( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-progress", + ); + const completed = thread.activities.find( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-named-task-completed", + ); + + const progressPayload = + progress?.payload && typeof progress.payload === "object" + ? (progress.payload as Record) + : undefined; + const completedPayload = + completed?.payload && typeof completed.payload === "object" + ? (completed.payload as Record) + : undefined; + + expect(progress?.summary).toBe("Typecheck mobile app"); + expect(progressPayload?.title).toBe("Typecheck mobile app"); + expect(completed?.summary).toBe("Task completed"); + expect(completedPayload?.title).toBe("Typecheck mobile app"); + expect(completedPayload?.summary).toBe("Typecheck finished without errors."); + expect(completedPayload?.detail).toBe("Typecheck finished without errors."); + }); + + it("titles task completion from task.started when no progress event carried the name", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + + harness.emit({ + type: "task.started", + eventId: asEventId("evt-fast-task-started"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-fast-task"), + payload: { + taskId: "fast-task-1", + description: "wait for codex review to finish", + taskType: "local_bash", + }, + }); + + harness.emit({ + type: "task.completed", + eventId: asEventId("evt-fast-task-completed"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-fast-task"), + payload: { + taskId: "fast-task-1", + status: "completed", + }, + }); + + const thread = await waitForThread(harness.readModel, (entry) => + entry.activities.some( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed", + ), + ); + + const completed = thread.activities.find( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-fast-task-completed", + ); + const completedPayload = + completed?.payload && typeof completed.payload === "object" + ? (completed.payload as Record) + : undefined; + + expect(completedPayload?.title).toBe("wait for codex review to finish"); + }); + + it("titles task completion from persisted activities after the description cache is swept", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + + harness.emit({ + type: "task.progress", + eventId: asEventId("evt-swept-task-progress"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-swept-task"), + payload: { + taskId: "swept-task-1", + description: "Watch round-3 CI and bots", + summary: "Polling CI checks.", + }, + }); + + await waitForThread(harness.readModel, (entry) => + entry.activities.some( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-progress", + ), + ); + + // session.exited sweeps the in-memory description cache; the completion + // that follows must recover the name from persisted activities. + harness.emit({ + type: "session.exited", + eventId: asEventId("evt-swept-task-session-exited"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + payload: {}, + }); + + harness.emit({ + type: "task.completed", + eventId: asEventId("evt-swept-task-completed"), + provider: ProviderDriverKind.make("claudeAgent"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-swept-task"), + payload: { + taskId: "swept-task-1", + status: "completed", + summary: "CI is green.", + }, + }); + + const thread = await waitForThread(harness.readModel, (entry) => + entry.activities.some( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed", + ), + ); + + const completed = thread.activities.find( + (activity: ProviderRuntimeTestActivity) => activity.id === "evt-swept-task-completed", + ); + const completedPayload = + completed?.payload && typeof completed.payload === "object" + ? (completed.payload as Record) + : undefined; + + expect(completedPayload?.title).toBe("Watch round-3 CI and bots"); + }); + it("projects structured user input request and resolution as thread activities", async () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 3e5978f4846..ef3454348e4 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -40,6 +40,42 @@ import { import { ServerSettingsService } from "../../serverSettings.ts"; const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; +const providerTaskKey = (threadId: ThreadId, taskId: string) => `${threadId}:${taskId}`; + +// Fallback when the in-memory description cache no longer has the task name +// (server restart, session-exit sweep, TTL/capacity eviction): earlier +// task.started/task.progress activities for the task are persisted with it. +function findTaskTitleInActivities( + activities: ReadonlyArray | undefined, + taskId: string, +): string | undefined { + if (!activities) { + return undefined; + } + for (let index = activities.length - 1; index >= 0; index -= 1) { + const activity = activities[index]; + if (!activity || (activity.kind !== "task.started" && activity.kind !== "task.progress")) { + continue; + } + const payload = + activity.payload && typeof activity.payload === "object" + ? (activity.payload as { taskId?: unknown; title?: unknown; detail?: unknown }) + : undefined; + if (payload?.taskId !== taskId) { + continue; + } + const title = + typeof payload.title === "string" + ? payload.title + : activity.kind === "task.started" && typeof payload.detail === "string" + ? payload.detail + : undefined; + if (title && title.trim().length > 0) { + return title; + } + } + return undefined; +} interface AssistantSegmentState { baseKey: string; @@ -53,6 +89,8 @@ const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); const BUFFERED_PROPOSED_PLAN_BY_ID_CACHE_CAPACITY = 10_000; const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = Duration.minutes(120); +const TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY = 10_000; +const TASK_DESCRIPTION_BY_TASK_TTL = Duration.minutes(120); const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.T3CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0"; @@ -264,6 +302,7 @@ function requestKindFromCanonicalRequestType( function runtimeEventToActivities( event: ProviderRuntimeEvent, + taskTitle?: string, ): ReadonlyArray { const maybeSequence = (() => { const eventWithSequence = event as ProviderRuntimeEvent & { sessionSequence?: number }; @@ -473,9 +512,15 @@ function runtimeEventToActivities( createdAt: event.createdAt, tone: "info", kind: "task.progress", - summary: "Reasoning update", + summary: + event.payload.description.trim().length > 0 + ? truncateDetail(event.payload.description, 120) + : "Reasoning update", payload: { taskId: event.payload.taskId, + ...(event.payload.description.trim().length > 0 + ? { title: truncateDetail(event.payload.description, 120) } + : {}), detail: truncateDetail(event.payload.summary ?? event.payload.description), ...(event.payload.summary ? { summary: truncateDetail(event.payload.summary) } : {}), ...(event.payload.lastToolName ? { lastToolName: event.payload.lastToolName } : {}), @@ -503,7 +548,15 @@ function runtimeEventToActivities( payload: { taskId: event.payload.taskId, status: event.payload.status, - ...(event.payload.summary ? { detail: truncateDetail(event.payload.summary) } : {}), + ...(taskTitle ? { title: truncateDetail(taskTitle, 120) } : {}), + // summary + detail mirror task.progress: clients label the row from + // summary and keep detail for the preview/expanded body. + ...(event.payload.summary + ? { + summary: truncateDetail(event.payload.summary), + detail: truncateDetail(event.payload.summary), + } + : {}), ...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}), }, turnId: toTurnId(event.turnId) ?? null, @@ -666,6 +719,27 @@ const make = Effect.gen(function* () { lookup: () => Effect.succeed({ text: "", createdAt: "" }), }); + // Task names arrive on task.started/task.progress but not on task.completed, + // so remember them per task to title the completion activity. + const taskDescriptionByTaskKey = yield* Cache.make({ + capacity: TASK_DESCRIPTION_BY_TASK_CACHE_CAPACITY, + timeToLive: TASK_DESCRIPTION_BY_TASK_TTL, + lookup: () => Effect.succeed(""), + }); + + const rememberTaskDescription = (threadId: ThreadId, taskId: string, description: string) => + Cache.set(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId), description); + + // Entries are left in place after completion so replayed or duplicate + // terminal events stay titled; TTL, capacity, and the session-exit sweep + // bound the cache. + const lookupTaskDescription = (threadId: ThreadId, taskId: string) => + Cache.getOption(taskDescriptionByTaskKey, providerTaskKey(threadId, taskId)).pipe( + Effect.map((description) => + Option.filter(description, (value) => value.length > 0).pipe(Option.getOrUndefined), + ), + ); + const resolveThreadDetail = Effect.fn("resolveThreadDetail")(function* (threadId: ThreadId) { return yield* projectionSnapshotQuery .getThreadDetailById(threadId) @@ -1090,6 +1164,7 @@ const make = Effect.gen(function* () { const turnKeys = Array.from(yield* Cache.keys(turnMessageIdsByTurnKey)); const assistantSegmentKeys = Array.from(yield* Cache.keys(assistantSegmentStateByTurnKey)); const proposedPlanKeys = Array.from(yield* Cache.keys(bufferedProposedPlanById)); + const taskDescriptionKeys = Array.from(yield* Cache.keys(taskDescriptionByTaskKey)); yield* Effect.forEach( turnKeys, (key) => @@ -1125,6 +1200,12 @@ const make = Effect.gen(function* () { : Effect.void, { concurrency: 1 }, ).pipe(Effect.asVoid); + yield* Effect.forEach( + taskDescriptionKeys, + (key) => + key.startsWith(prefix) ? Cache.invalidate(taskDescriptionByTaskKey, key) : Effect.void, + { concurrency: 1 }, + ).pipe(Effect.asVoid); }); const getSourceProposedPlanReferenceForPendingTurnStart = Effect.fn( @@ -1654,7 +1735,22 @@ const make = Effect.gen(function* () { } } - const activities = runtimeEventToActivities(event); + if (event.type === "task.started" || event.type === "task.progress") { + const description = event.payload.description?.trim(); + if (description) { + yield* rememberTaskDescription(thread.id, event.payload.taskId, description); + } + } + let taskTitle: string | undefined; + if (event.type === "task.completed") { + taskTitle = yield* lookupTaskDescription(thread.id, event.payload.taskId); + if (!taskTitle) { + const threadDetail = yield* getLoadedThreadDetail(); + taskTitle = findTaskTitleInActivities(threadDetail?.activities, event.payload.taskId); + } + } + + const activities = runtimeEventToActivities(event, taskTitle); yield* Effect.forEach(activities, (activity) => providerCommandId(event, "thread-activity-append").pipe( Effect.flatMap((commandId) => From 3235658c080bc12fcd1ffaa275aced98d225f2f6 Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Mon, 20 Jul 2026 09:43:07 +0100 Subject: [PATCH 21/39] fix: delegate OpenCode session titles to provider (#3720) --- .../provider/Layers/OpenCodeAdapter.test.ts | 46 +++++++++++++++- .../src/provider/Layers/OpenCodeAdapter.ts | 53 +++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts index b227ff1ab66..4358cf305b0 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts @@ -54,6 +54,7 @@ const runtimeMock = { state: { startCalls: [] as string[], sessionCreateUrls: [] as string[], + sessionCreateInputs: [] as Array>, authHeaders: [] as Array, abortCalls: [] as string[], closeCalls: [] as string[], @@ -67,6 +68,7 @@ const runtimeMock = { reset() { this.state.startCalls.length = 0; this.state.sessionCreateUrls.length = 0; + this.state.sessionCreateInputs.length = 0; this.state.authHeaders.length = 0; this.state.abortCalls.length = 0; this.state.closeCalls.length = 0; @@ -122,8 +124,9 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { createOpenCodeSdkClient: ({ baseUrl, serverPassword }) => ({ session: { - create: async () => { + create: async (input: Record) => { runtimeMock.state.sessionCreateUrls.push(baseUrl); + runtimeMock.state.sessionCreateInputs.push(input); runtimeMock.state.authHeaders.push( serverPassword ? `Basic ${btoa(`opencode:${serverPassword}`)}` : null, ); @@ -765,6 +768,47 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { }), ); + it.effect("lets OpenCode own session title generation and emits title metadata updates", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-title-sync"); + runtimeMock.state.subscribedEvents = [ + { + type: "session.updated", + properties: { + info: { + id: "http://127.0.0.1:9999/session", + title: "Investigate OpenCode title sync", + }, + }, + }, + ]; + + const eventsFiber = yield* adapter.streamEvents.pipe( + Stream.filter((event) => event.threadId === threadId), + Stream.take(3), + Stream.runCollect, + Effect.forkChild, + ); + + yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + }); + + const events = Array.from(yield* Fiber.join(eventsFiber).pipe(Effect.timeout("1 second"))); + NodeAssert.equal(runtimeMock.state.sessionCreateInputs.length, 1); + NodeAssert.equal("title" in (runtimeMock.state.sessionCreateInputs[0] ?? {}), false); + + const metadataUpdated = events.find((event) => event.type === "thread.metadata.updated"); + NodeAssert.ok(metadataUpdated); + if (metadataUpdated.type === "thread.metadata.updated") { + NodeAssert.equal(metadataUpdated.payload.name, "Investigate OpenCode title sync"); + } + }), + ); + it.effect("writes provider-native observability records using the session thread id", () => Effect.gen(function* () { const nativeEvents: Array<{ diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts index 956905e3a3c..e7622e6c705 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts @@ -65,6 +65,35 @@ type OpenCodeSubscribedEvent = ? TEvent : never; +function trimText(value: string | undefined | null): string | undefined { + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : undefined; +} + +function openCodeEventSessionId(event: OpenCodeSubscribedEvent): string | undefined { + const properties = "properties" in event ? event.properties : undefined; + if (!properties || typeof properties !== "object") { + return undefined; + } + + const sessionID = (properties as { readonly sessionID?: unknown }).sessionID; + const sessionIDFromProperties = typeof sessionID === "string" ? sessionID : undefined; + if (sessionIDFromProperties) { + return sessionIDFromProperties; + } + + const info = (properties as { readonly info?: { readonly id?: unknown } }).info; + return info && typeof info.id === "string" ? info.id : undefined; +} + +function openCodeEventSessionTitle(event: OpenCodeSubscribedEvent): string | undefined { + if (event.type !== "session.updated") { + return undefined; + } + + return trimText(event.properties.info.title); +} + interface OpenCodeSessionContext { session: ProviderSession; readonly client: OpencodeClient; @@ -643,8 +672,7 @@ export function makeOpenCodeAdapter( context: OpenCodeSessionContext, event: OpenCodeSubscribedEvent, ) { - const payloadSessionId = - "properties" in event ? (event.properties as { sessionID?: unknown }).sessionID : undefined; + const payloadSessionId = openCodeEventSessionId(event); if (payloadSessionId !== context.openCodeSessionId) { return; } @@ -663,6 +691,26 @@ export function makeOpenCodeAdapter( }); switch (event.type) { + case "session.updated": { + const title = openCodeEventSessionTitle(event); + if (title) { + yield* emit({ + ...(yield* buildEventBase({ + threadId: context.session.threadId, + raw: event, + })), + type: "thread.metadata.updated", + payload: { + name: title, + metadata: { + sessionID: context.openCodeSessionId, + }, + }, + }); + } + break; + } + case "message.updated": { context.messageRoleById.set(event.properties.info.id, event.properties.info.role); if (event.properties.info.role === "assistant") { @@ -1069,7 +1117,6 @@ export function makeOpenCodeAdapter( } const openCodeSession = yield* runOpenCodeSdk("session.create", () => client.session.create({ - title: `T3 Code ${input.threadId}`, permission: buildOpenCodePermissionRules(input.runtimeMode), }), ); From df65a6c3ad605feb2f326c0ad78d056c10476770 Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Mon, 20 Jul 2026 10:51:22 +0200 Subject: [PATCH 22/39] Archive selected threads from the context menu (#3895) --- apps/web/src/components/Sidebar.logic.test.ts | 69 +++++++++++++++++++ apps/web/src/components/Sidebar.logic.ts | 50 ++++++++++++++ apps/web/src/components/Sidebar.tsx | 69 ++++++++++++++++--- apps/web/src/hooks/useThreadActions.ts | 6 +- 4 files changed, 180 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 5f6c234e5f6..1dd4e340125 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -1,5 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { + archiveSelectedThreadEntries, + buildMultiSelectThreadContextMenuItems, createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, getVisibleSidebarThreadIds, @@ -38,6 +40,73 @@ import { const localEnvironmentId = EnvironmentId.make("environment-local"); +describe("archiveSelectedThreadEntries", () => { + const entries = [{ threadKey: "one" }, { threadKey: "two" }, { threadKey: "three" }] as const; + const success = { _tag: "Success" } as const; + const failure = { _tag: "Failure" } as const; + + it("records every entry after full success", async () => { + const outcome = await archiveSelectedThreadEntries({ + entries, + archive: async (_entry, onArchived) => { + onArchived(); + return success; + }, + }); + + expect(outcome).toEqual({ + archivedThreadKeys: ["one", "two", "three"], + mutationFailure: null, + followupFailures: [], + }); + }); + + it("stops at a mutation failure and retains prior successes", async () => { + const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { + if (entry.threadKey === "two") return failure; + onArchived(); + return success; + }); + const outcome = await archiveSelectedThreadEntries({ entries, archive }); + + expect(archive).toHaveBeenCalledTimes(2); + expect(outcome).toEqual({ + archivedThreadKeys: ["one"], + mutationFailure: failure, + followupFailures: [], + }); + }); + + it("continues after a post-archive failure", async () => { + const archive = vi.fn(async (entry: (typeof entries)[number], onArchived: () => void) => { + onArchived(); + return entry.threadKey === "two" ? failure : success; + }); + const outcome = await archiveSelectedThreadEntries({ entries, archive }); + + expect(archive).toHaveBeenCalledTimes(3); + expect(outcome).toEqual({ + archivedThreadKeys: ["one", "two", "three"], + mutationFailure: null, + followupFailures: [failure], + }); + }); +}); + +describe("buildMultiSelectThreadContextMenuItems", () => { + it("offers bulk archive with the selected count", () => { + expect( + buildMultiSelectThreadContextMenuItems({ count: 3, hasRunningThread: false }), + ).toContainEqual({ id: "archive", label: "Archive (3)", disabled: false }); + }); + + it("disables bulk archive when a selected thread is running", () => { + expect( + buildMultiSelectThreadContextMenuItems({ count: 2, hasRunningThread: true }), + ).toContainEqual({ id: "archive", label: "Archive (2)", disabled: true }); + }); +}); + describe("resolveSidebarStageBadgeLabel", () => { it("returns Nightly for nightly primary server versions", () => { expect( diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 91a61cc1558..1a565efd878 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import type { ContextMenuItem } from "@t3tools/contracts"; import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings"; import { getThreadSortTimestamp, @@ -36,6 +37,55 @@ type ScopedSidebarThread = ThreadSortInput & { export type ThreadTraversalDirection = "previous" | "next"; +export async function archiveSelectedThreadEntries< + TEntry extends { readonly threadKey: string }, + TResult extends { readonly _tag: "Success" | "Failure" }, +>(input: { + entries: readonly TEntry[]; + archive: (entry: TEntry, onArchived: () => void) => Promise; +}): Promise<{ + archivedThreadKeys: readonly string[]; + mutationFailure: Extract | null; + followupFailures: readonly Extract[]; +}> { + const archivedThreadKeys: string[] = []; + const followupFailures: Extract[] = []; + + for (const entry of input.entries) { + let didArchive = false; + const result = await input.archive(entry, () => { + didArchive = true; + }); + if (didArchive || result._tag === "Success") { + archivedThreadKeys.push(entry.threadKey); + } + if (result._tag === "Success") continue; + const failure = result as Extract; + if (didArchive) { + followupFailures.push(failure); + continue; + } + return { archivedThreadKeys, mutationFailure: failure, followupFailures }; + } + + return { archivedThreadKeys, mutationFailure: null, followupFailures }; +} + +export function buildMultiSelectThreadContextMenuItems(input: { + count: number; + hasRunningThread: boolean; +}): readonly ContextMenuItem<"mark-unread" | "archive" | "delete">[] { + return [ + { id: "mark-unread", label: `Mark unread (${input.count})` }, + { + id: "archive", + label: `Archive (${input.count})`, + disabled: input.hasRunningThread, + }, + { id: "delete", label: `Delete (${input.count})`, destructive: true }, + ]; +} + export interface ThreadStatusPill { label: | "Working" diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 715a94e9672..4b4b1e52a94 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -185,6 +185,8 @@ import { import { useThreadSelectionStore } from "../threadSelectionStore"; import { useOpenAddProjectCommandPalette } from "../commandPaletteContext"; import { + archiveSelectedThreadEntries, + buildMultiSelectThreadContextMenuItems, getSidebarThreadIdsToPrewarm, resolveAdjacentThreadId, isContextMenuPointerDown, @@ -1775,24 +1777,69 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec const threadKeys = [...useThreadSelectionStore.getState().selectedThreadKeys]; if (threadKeys.length === 0) return; const count = threadKeys.length; + const selectedThreadEntries = threadKeys.flatMap((threadKey) => { + const threadRef = parseScopedThreadKey(threadKey); + const thread = threadRef ? readThreadShell(threadRef) : null; + return threadRef && thread ? [{ threadKey, threadRef, thread }] : []; + }); + const hasRunningThread = selectedThreadEntries.some( + ({ thread }) => thread.session?.status === "running" && thread.session.activeTurnId != null, + ); const clicked = await api.contextMenu.show( - [ - { id: "mark-unread", label: `Mark unread (${count})` }, - { id: "delete", label: `Delete (${count})`, destructive: true }, - ], + buildMultiSelectThreadContextMenuItems({ count, hasRunningThread }), position, ); if (clicked === "mark-unread") { - for (const threadKey of threadKeys) { - const thread = sidebarThreadByKeyRef.current.get(threadKey); - markThreadUnread(threadKey, thread?.latestTurn?.completedAt); + for (const { threadKey, thread } of selectedThreadEntries) { + markThreadUnread(threadKey, thread.latestTurn?.completedAt); } clearSelection(); return; } + if (clicked === "archive") { + if (appSettingsConfirmThreadArchive) { + const confirmed = await api.dialogs.confirm( + `Archive ${count} thread${count === 1 ? "" : "s"}?`, + ); + if (!confirmed) return; + } + + const archiveOutcome = await archiveSelectedThreadEntries({ + entries: selectedThreadEntries, + archive: ({ threadRef }, onArchived) => archiveThread(threadRef, { onArchived }), + }); + for (const failure of archiveOutcome.followupFailures) { + if (isAtomCommandInterrupted(failure)) continue; + const error = squashAtomCommandFailure(failure); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Thread archived, but navigation failed", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + if (archiveOutcome.mutationFailure) { + removeFromSelection(archiveOutcome.archivedThreadKeys); + if (!isAtomCommandInterrupted(archiveOutcome.mutationFailure)) { + const error = squashAtomCommandFailure(archiveOutcome.mutationFailure); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to archive threads", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + return; + } + removeFromSelection(threadKeys); + return; + } + if (clicked !== "delete") return; if (appSettingsConfirmThreadDelete) { @@ -1806,10 +1853,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec } const deletedThreadKeys = new Set(threadKeys); - for (const threadKey of threadKeys) { - const thread = sidebarThreadByKeyRef.current.get(threadKey); - if (!thread) continue; - const result = await deleteThread(scopeThreadRef(thread.environmentId, thread.id), { + for (const { threadRef } of selectedThreadEntries) { + const result = await deleteThread(threadRef, { deletedThreadKeys, }); if (result._tag === "Failure") { @@ -1829,7 +1874,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec removeFromSelection(threadKeys); }, [ + appSettingsConfirmThreadArchive, appSettingsConfirmThreadDelete, + archiveThread, clearSelection, deleteThread, markThreadUnread, diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts index 07655ad30d7..dbde5acce99 100644 --- a/apps/web/src/hooks/useThreadActions.ts +++ b/apps/web/src/hooks/useThreadActions.ts @@ -89,7 +89,7 @@ export function useThreadActions() { }, [router]); const archiveThread = useCallback( - async (target: ScopedThreadRef) => { + async (target: ScopedThreadRef, opts: { onArchived?: () => void } = {}) => { const resolved = resolveThreadTarget(target); if (!resolved) return AsyncResult.success(undefined); const { thread, threadRef } = resolved; @@ -115,6 +115,8 @@ export function useThreadActions() { if (archiveResult._tag === "Failure") { return archiveResult; } + refreshArchivedThreadsForEnvironment(threadRef.environmentId); + opts.onArchived?.(); if (shouldNavigateToDraft) { const navigationResult = await settlePromise(() => @@ -123,11 +125,9 @@ export function useThreadActions() { if (navigationResult._tag === "Failure") { return navigationResult; } - refreshArchivedThreadsForEnvironment(threadRef.environmentId); return archiveResult; } - refreshArchivedThreadsForEnvironment(threadRef.environmentId); return archiveResult; }, [archiveThreadMutation, getCurrentRouteThreadRef, resolveThreadTarget], From 5fcfe242c1643eb5cee3699b160f59ad5f969fca Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Mon, 20 Jul 2026 04:53:02 -0400 Subject: [PATCH 23/39] fix(cli): support force removing projects (#3922) --- apps/server/src/bin.test.ts | 66 +++++++++++++++++++++++++++++++++- apps/server/src/cli/project.ts | 5 +++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/apps/server/src/bin.test.ts b/apps/server/src/bin.test.ts index 16b7a7f715a..999547b71d8 100644 --- a/apps/server/src/bin.test.ts +++ b/apps/server/src/bin.test.ts @@ -6,10 +6,16 @@ import * as NodePath from "node:path"; import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { EnvironmentOrchestrationHttpApi } from "@t3tools/contracts"; +import { + CommandId, + EnvironmentOrchestrationHttpApi, + ProviderInstanceId, + ThreadId, +} from "@t3tools/contracts"; import * as NetService from "@t3tools/shared/Net"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as DateTime from "effect/DateTime"; import * as Layer from "effect/Layer"; import * as HttpRouter from "effect/unstable/http/HttpRouter"; import * as HttpServer from "effect/unstable/http/HttpServer"; @@ -22,6 +28,7 @@ import { Command } from "effect/unstable/cli"; import { cli, makeCli } from "./bin.ts"; import * as ServerConfig from "./config.ts"; import * as ProjectionSnapshotQuery from "./orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as OrchestrationEngine from "./orchestration/Services/OrchestrationEngine.ts"; import { OrchestrationLayerLive } from "./orchestration/runtimeLayer.ts"; import { orchestrationHttpApiLayer } from "./orchestration/http.ts"; import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/Sqlite.ts"; @@ -460,6 +467,63 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => { }), ); + it.effect("force removes projects that still contain threads", () => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-force-remove-test-"), + ); + const workspaceRoot = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-cli-projects-force-remove-workspace-"), + ); + + yield* runCliWithRuntime(["project", "add", workspaceRoot, "--base-dir", baseDir]); + const afterAdd = yield* readPersistedSnapshot(baseDir); + const project = afterAdd.projects.find( + (candidate) => candidate.workspaceRoot === workspaceRoot && candidate.deletedAt === null, + ); + assert.isTrue(project !== undefined); + + const config = yield* makeCliTestServerConfig(baseDir); + yield* Effect.gen(function* () { + const engine = yield* OrchestrationEngine.OrchestrationEngineService; + yield* engine.dispatch({ + type: "thread.create", + commandId: CommandId.make("cmd-cli-force-remove-thread"), + threadId: ThreadId.make("thread-cli-force-remove"), + projectId: project!.id, + title: "Thread", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + interactionMode: "default", + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: DateTime.formatIso(yield* DateTime.now), + }); + }).pipe(Effect.provide(makeProjectPersistenceLayer(config))); + + yield* runCliWithRuntime([ + "project", + "remove", + project!.id, + "--force", + "--base-dir", + baseDir, + ]); + const afterRemove = yield* readPersistedSnapshot(baseDir); + assert.isTrue( + (afterRemove.projects.find((candidate) => candidate.id === project!.id)?.deletedAt ?? + null) !== null, + ); + assert.isTrue( + (afterRemove.threads.find((thread) => thread.id === "thread-cli-force-remove")?.deletedAt ?? + null) !== null, + ); + }), + ); + it.effect("routes project commands through a running server when runtime state is present", () => Effect.gen(function* () { const baseDir = NodeFS.mkdtempSync( diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 710d39c4c29..25733a5e35b 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -493,6 +493,10 @@ const projectRemoveCommand = Command.make("remove", { project: Argument.string("project").pipe( Argument.withDescription("Project id or workspace root to remove."), ), + force: Flag.boolean("force").pipe( + Flag.withDescription("Delete the project and all of its threads."), + Flag.withDefault(false), + ), }).pipe( Command.withDescription("Remove a project."), Command.withHandler((flags) => @@ -515,6 +519,7 @@ const projectRemoveCommand = Command.make("remove", { type: "project.delete", commandId: CommandId.make(yield* projectCommandUuid), projectId: project.id, + force: flags.force, }); return `Removed project ${project.id} (${project.title}).`; }), From d266c068d0e8b8cd168d38cd70b885a7a9e233e2 Mon Sep 17 00:00:00 2001 From: Shoaib Date: Mon, 20 Jul 2026 14:28:20 +0530 Subject: [PATCH 24/39] fix: allow sidebar to be shrunk when wider than viewport (#2456) Co-authored-by: Shoaib Ansari Co-authored-by: Julius Marminge --- apps/web/src/components/AppSidebarLayout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 4d0486aa866..ee56858bc58 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -138,7 +138,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { className="border-r border-border bg-card text-foreground" resizable={{ minWidth: THREAD_SIDEBAR_MIN_WIDTH, - shouldAcceptWidth: ({ nextWidth, wrapper }) => + shouldAcceptWidth: ({ currentWidth, nextWidth, wrapper }) => + nextWidth <= currentWidth || wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, }} From 33f1cb422461bf0f072b9f181734e9b654d1ca69 Mon Sep 17 00:00:00 2001 From: Guilherme Vieira <46866023+GuilhermeVieiraDev@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:58:38 +0100 Subject: [PATCH 25/39] fix(codex): show web search query and url in tool call details (#2093) Co-authored-by: Julius Marminge --- apps/server/src/provider/Layers/CodexAdapter.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 270126e934b..4cbaf299429 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -267,8 +267,14 @@ function itemTitle(itemType: CanonicalItemType, item?: CodexLifecycleItem): stri } } -function itemDetail(item: CodexLifecycleItem): string | undefined { +function itemDetail(itemType: CanonicalItemType, item: CodexLifecycleItem): string | undefined { + const itemRecord = item as Record; + const action = itemRecord.action as Record | undefined; + const actionQueries = Array.isArray(action?.queries) ? action.queries : []; const candidates = [ + ...(itemType === "web_search" + ? [itemRecord.query, action?.query, ...actionQueries, action?.pattern, action?.url] + : []), "command" in item ? item.command : undefined, "title" in item ? item.title : undefined, "summary" in item ? item.summary : undefined, @@ -276,6 +282,7 @@ function itemDetail(item: CodexLifecycleItem): string | undefined { "path" in item ? item.path : undefined, "prompt" in item ? item.prompt : undefined, ]; + for (const candidate of candidates) { const trimmed = typeof candidate === "string" ? trimText(candidate) : undefined; if (!trimmed) continue; @@ -465,7 +472,7 @@ function mapItemLifecycle( return undefined; } - const detail = itemDetail(item); + const detail = itemDetail(itemType, item); const status = lifecycle === "item.started" ? "inProgress" @@ -839,7 +846,7 @@ function mapToRuntimeEvents( } const itemType = toCanonicalItemType(item.type); if (itemType === "plan") { - const detail = itemDetail(item); + const detail = itemDetail(itemType, item); if (!detail) { return []; } From 40c0ab088308fb010a3db488c545f10aa7179ed2 Mon Sep 17 00:00:00 2001 From: James <105842516+jamesx0416@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:04:03 +1000 Subject: [PATCH 26/39] Add Codex launch arguments setting (#2892) Co-authored-by: Julius Marminge Co-authored-by: Julius Marminge Co-authored-by: root --- .../src/provider/Layers/CodexAdapter.test.ts | 64 +++++++++++++++++ .../src/provider/Layers/CodexAdapter.ts | 2 + .../src/provider/Layers/CodexProvider.ts | 16 +++-- .../Layers/CodexSessionRuntime.test.ts | 28 ++++++++ .../provider/Layers/CodexSessionRuntime.ts | 12 ++-- .../ProviderInstanceRegistryLive.test.ts | 1 + .../provider/Layers/ProviderRegistry.test.ts | 16 +++++ .../provider/Layers/codexLaunchArgs.test.ts | 59 +++++++++++++++ .../src/provider/Layers/codexLaunchArgs.ts | 48 +++++++++++++ apps/server/src/serverSettings.test.ts | 2 + .../CodexTextGeneration.test.ts | 72 ++++++++++++++++++- .../src/textGeneration/CodexTextGeneration.ts | 3 + .../settings/ProviderSettingsForm.test.ts | 1 + packages/contracts/src/settings.test.ts | 4 ++ packages/contracts/src/settings.ts | 10 ++- packages/shared/src/cliArgs.test.ts | 30 +++++++- packages/shared/src/cliArgs.ts | 62 +++++++++++++++- 17 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 apps/server/src/provider/Layers/codexLaunchArgs.test.ts create mode 100644 apps/server/src/provider/Layers/codexLaunchArgs.ts diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 515a7c6fcbb..4ae654a5187 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -279,6 +279,7 @@ validationLayer("CodexAdapterLive validation", (it) => { NodeAssert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], { binaryPath: "codex", cwd: process.cwd(), + launchArgs: "", model: "gpt-5.3-codex", providerInstanceId: ProviderInstanceId.make("codex"), serviceTier: "priority", @@ -359,6 +360,69 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }), ); + it.effect("passes configured launch args into the session runtime", () => { + const runtimeFactory = makeRuntimeFactory(); + const layer = Layer.effect( + CodexAdapter, + Effect.gen(function* () { + const codexConfig = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" }); + return yield* makeCodexAdapter(codexConfig, { + makeRuntime: runtimeFactory.factory, + }); + }), + ).pipe( + Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), + Layer.provideMerge(ServerSettingsService.layerTest()), + Layer.provideMerge(providerSessionDirectoryTestLayer), + Layer.provideMerge(NodeServices.layer), + ); + + return Effect.gen(function* () { + const adapter = yield* CodexAdapter; + yield* adapter.startSession({ + provider: ProviderDriverKind.make("codex"), + threadId: asThreadId("sess-launch-args"), + runtimeMode: "full-access", + }); + + const runtime = runtimeFactory.lastRuntime; + NodeAssert.ok(runtime); + NodeAssert.equal(runtime.options.launchArgs, "--strict-config --enable foo"); + }).pipe(Effect.provide(layer)); + }); + + it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for the session runtime", () => { + const runtimeFactory = makeRuntimeFactory(); + const layer = Layer.effect( + CodexAdapter, + Effect.gen(function* () { + const codexConfig = decodeCodexSettings({ launchArgs: "--enable settings-feature" }); + return yield* makeCodexAdapter(codexConfig, { + environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --enable env-feature " }, + makeRuntime: runtimeFactory.factory, + }); + }), + ).pipe( + Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), + Layer.provideMerge(ServerSettingsService.layerTest()), + Layer.provideMerge(providerSessionDirectoryTestLayer), + Layer.provideMerge(NodeServices.layer), + ); + + return Effect.gen(function* () { + const adapter = yield* CodexAdapter; + yield* adapter.startSession({ + provider: ProviderDriverKind.make("codex"), + threadId: asThreadId("sess-launch-args-env"), + runtimeMode: "full-access", + }); + + const runtime = runtimeFactory.lastRuntime; + NodeAssert.ok(runtime); + NodeAssert.equal(runtime.options.launchArgs, "--strict-config --enable env-feature"); + }).pipe(Effect.provide(layer)); + }); + it.effect("maps codex model options for the adapter's bound custom instance id", () => { const customInstanceId = ProviderInstanceId.make("codex_personal"); const customRuntimeFactory = makeRuntimeFactory(); diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 4cbaf299429..38a5887cdc3 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -61,6 +61,7 @@ import { type CodexSessionRuntimeShape, } from "./CodexSessionRuntime.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; +import { resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); const isCodexSessionRuntimeThreadIdMissingError = Schema.is( @@ -1399,6 +1400,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( providerInstanceId: boundInstanceId, cwd: input.cwd ?? process.cwd(), binaryPath: codexConfig.binaryPath, + launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment), ...(options?.environment ? { environment: options.environment } : {}), ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), ...(isCodexResumeCursorSchema(input.resumeCursor) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index a2182cfb73c..9306087a0bc 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -26,6 +26,7 @@ import { ServerSettingsError } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; import { AUTH_PROBE_TIMEOUT_MS, buildServerProvider, @@ -289,6 +290,7 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams { const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { readonly binaryPath: string; readonly homePath?: string; + readonly launchArgs?: string; readonly cwd: string; readonly customModels?: ReadonlyArray; readonly environment?: NodeJS.ProcessEnv; @@ -303,10 +305,14 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun ...input.environment, ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}), }; - const spawnCommand = yield* resolveSpawnCommand(input.binaryPath, ["app-server"], { - env: environment, - extendEnv: true, - }); + const spawnCommand = yield* resolveSpawnCommand( + input.binaryPath, + codexAppServerArgs(input.launchArgs), + { + env: environment, + extendEnv: true, + }, + ); const child = yield* spawner .spawn( ChildProcess.make(spawnCommand.command, spawnCommand.args, { @@ -465,6 +471,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu probe: (input: { readonly binaryPath: string; readonly homePath?: string; + readonly launchArgs?: string; readonly cwd: string; readonly customModels: ReadonlyArray; readonly environment?: NodeJS.ProcessEnv; @@ -503,6 +510,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu const probeResult = yield* probe({ binaryPath: codexSettings.binaryPath, homePath: codexSettings.homePath, + launchArgs: resolveCodexLaunchArgs(codexSettings.launchArgs, resolvedEnvironment), cwd: process.cwd(), customModels: codexSettings.customModels, environment: resolvedEnvironment, diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 1527072dae7..119fa36303a 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -13,6 +13,7 @@ import { CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, } from "../CodexDeveloperInstructions.ts"; +import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { buildTurnStartParams, hasConfiguredMcpServer, @@ -289,6 +290,33 @@ describe("hasConfiguredMcpServer", () => { }); }); +describe("codexSessionAppServerArgs", () => { + it("keeps the app-server subcommand when explicit args are provided", () => { + NodeAssert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [ + "app-server", + "-c", + "model=gpt-5", + ]); + }); + + it("keeps launch args when explicit app-server args are provided", () => { + NodeAssert.deepStrictEqual( + codexSessionAppServerArgs( + ["-c", "mcp_servers.t3-code.url=http://127.0.0.1/mcp"], + "--strict-config --enable foo", + ), + [ + "app-server", + "--strict-config", + "--enable", + "foo", + "-c", + "mcp_servers.t3-code.url=http://127.0.0.1/mcp", + ], + ); + }); +}); + describe("isRecoverableThreadResumeError", () => { it("matches missing thread errors", () => { NodeAssert.equal( diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 91938b5355d..5a81e915e34 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -36,6 +36,7 @@ import * as CodexRpc from "effect-codex-app-server/rpc"; import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts"; +import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts"; const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse); @@ -97,6 +98,7 @@ export interface CodexSessionRuntimeOptions { readonly providerInstanceId?: ProviderInstanceId; readonly binaryPath: string; readonly homePath?: string; + readonly launchArgs?: string; readonly environment?: NodeJS.ProcessEnv; readonly cwd: string; readonly runtimeMode: RuntimeMode; @@ -717,11 +719,11 @@ export const makeCodexSessionRuntime = ( ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}), }; const extendEnv = options.environment === undefined; - const spawnCommand = yield* resolveSpawnCommand( - options.binaryPath, - ["app-server", ...(options.appServerArgs ?? [])], - { env, extendEnv }, - ); + const appServerArgs = codexSessionAppServerArgs(options.appServerArgs, options.launchArgs); + const spawnCommand = yield* resolveSpawnCommand(options.binaryPath, appServerArgs, { + env, + extendEnv, + }); const child = yield* spawner .spawn( ChildProcess.make(spawnCommand.command, spawnCommand.args, { diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts index 73390450efa..384de852f9b 100644 --- a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts +++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts @@ -61,6 +61,7 @@ const makeCodexConfig = (overrides: Partial): CodexSettings => ({ binaryPath: "codex", homePath: "", shadowHomePath: "", + launchArgs: "", customModels: [], ...overrides, }); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 78ff5cf493d..159d853121c 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -55,6 +55,7 @@ const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTING const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({}); const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({}); +const decodeCodexSettings = Schema.decodeSync(CodexSettings); const disabledCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({ enabled: false, }); @@ -348,6 +349,21 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te }), ); + it.effect("passes configured launch args to the Codex provider probe", () => + Effect.gen(function* () { + let observedLaunchArgs: string | undefined; + const settings = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" }); + + const status = yield* checkCodexProviderStatus(settings, (input) => { + observedLaunchArgs = input.launchArgs; + return Effect.succeed(makeCodexProbeSnapshot()); + }); + + assert.strictEqual(status.status, "ready"); + assert.strictEqual(observedLaunchArgs, "--strict-config --enable foo"); + }), + ); + it.effect("returns unauthenticated when app-server requires OpenAI auth", () => Effect.gen(function* () { const status = yield* checkCodexProviderStatus(defaultCodexSettings, () => diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts new file mode 100644 index 00000000000..115ac28eaf9 --- /dev/null +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -0,0 +1,59 @@ +import * as NodeAssert from "node:assert/strict"; + +import { describe, it } from "vite-plus/test"; + +import { + codexAppServerArgs, + codexExecLaunchArgs, + resolveCodexLaunchArgs, +} from "./codexLaunchArgs.ts"; + +describe("resolveCodexLaunchArgs", () => { + it("uses T3CODE_CODEX_LAUNCH_ARGS before configured settings", () => { + NodeAssert.equal( + resolveCodexLaunchArgs(" --strict-config ", { T3CODE_CODEX_LAUNCH_ARGS: "--enable foo" }), + "--enable foo", + ); + }); + + it("uses configured settings when T3CODE_CODEX_LAUNCH_ARGS is empty", () => { + NodeAssert.equal( + resolveCodexLaunchArgs(" --strict-config ", { T3CODE_CODEX_LAUNCH_ARGS: " " }), + "--strict-config", + ); + }); + + it("ignores whitespace-only environment values", () => { + NodeAssert.equal(resolveCodexLaunchArgs("", { T3CODE_CODEX_LAUNCH_ARGS: " " }), ""); + }); +}); + +describe("codexAppServerArgs", () => { + it("returns the app-server command for empty launch args", () => { + NodeAssert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]); + }); + + it("appends parsed launch args after app-server", () => { + NodeAssert.deepStrictEqual(codexAppServerArgs("--strict-config --enable foo"), [ + "app-server", + "--strict-config", + "--enable", + "foo", + ]); + }); +}); + +describe("codexExecLaunchArgs", () => { + it("keeps shared codex flags and omits app-server-only flags", () => { + NodeAssert.deepStrictEqual( + codexExecLaunchArgs('--strict-config --enable foo --listen off --config model="gpt 5"'), + ["--strict-config", "--enable", "foo", "--config", "model=gpt 5"], + ); + }); + + it("does not pair value-taking flags with adjacent flags", () => { + NodeAssert.deepStrictEqual(codexExecLaunchArgs("--config --strict-config --enable --disable"), [ + "--strict-config", + ]); + }); +}); diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.ts b/apps/server/src/provider/Layers/codexLaunchArgs.ts new file mode 100644 index 00000000000..771a4f0b6ed --- /dev/null +++ b/apps/server/src/provider/Layers/codexLaunchArgs.ts @@ -0,0 +1,48 @@ +import { tokenizeCliArgs } from "@t3tools/shared/cliArgs"; + +export const T3CODE_CODEX_LAUNCH_ARGS_ENV = "T3CODE_CODEX_LAUNCH_ARGS"; + +export const resolveCodexLaunchArgs = ( + launchArgs?: string, + environment: NodeJS.ProcessEnv = process.env, +) => environment[T3CODE_CODEX_LAUNCH_ARGS_ENV]?.trim() || launchArgs?.trim() || ""; + +export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => + tokenizeCliArgs(launchArgs); + +export const codexAppServerArgs = (launchArgs?: string) => [ + "app-server", + ...codexLaunchArgv(launchArgs), +]; + +export const codexExecLaunchArgs = (launchArgs?: string) => { + const args = codexLaunchArgv(launchArgs); + const execArgs: Array = []; + + for (let index = 0; index < args.length; index++) { + const arg = args[index]; + if (arg === undefined) continue; + + if (arg === "--strict-config" || arg.startsWith("--config=") || arg.startsWith("-c=")) { + execArgs.push(arg); + } else if (arg === "--config" || arg === "-c" || arg === "--enable" || arg === "--disable") { + const value = args[index + 1]; + if (value !== undefined && !value.startsWith("-")) { + execArgs.push(arg, value); + index++; + } + } else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) { + execArgs.push(arg); + } + } + + return execArgs; +}; + +export const codexSessionAppServerArgs = ( + appServerArgs: ReadonlyArray | undefined, + launchArgs: string | undefined, +) => { + const launchAppServerArgs = codexAppServerArgs(launchArgs); + return appServerArgs ? [...launchAppServerArgs, ...appServerArgs] : launchAppServerArgs; +}; diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index 504d99e18de..487ae9b45b8 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -179,6 +179,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { binaryPath: "/opt/homebrew/bin/codex", homePath: "/Users/julius/.codex", shadowHomePath: "", + launchArgs: "", customModels: [], }); assert.deepEqual(next.providers.claudeAgent, { @@ -420,6 +421,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { binaryPath: "/opt/homebrew/bin/codex", homePath: "", shadowHomePath: "", + launchArgs: "", customModels: [], }); assert.deepEqual(next.providers.claudeAgent, { diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts index 24054a95870..657118fff51 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts @@ -35,6 +35,8 @@ function makeFakeCodexBinary( requireServiceTier?: string; requireReasoningEffort?: string; forbidReasoningEffort?: boolean; + requireArg?: string; + forbidArg?: string; stdinMustContain?: string; stdinMustNotContain?: string; }, @@ -50,6 +52,7 @@ function makeFakeCodexBinary( codexPath, [ "#!/bin/sh", + 'original_args="$*"', 'output_path=""', 'seen_image="0"', 'seen_service_tier=""', @@ -87,6 +90,22 @@ function makeFakeCodexBinary( " shift", "done", 'stdin_content="$(cat)"', + ...(input.requireArg !== undefined + ? [ + `case " $original_args " in *" ${input.requireArg} "*) ;; *)`, + ` printf "%s\\n" "missing arg: ${input.requireArg}" >&2`, + ` exit 8`, + "esac", + ] + : []), + ...(input.forbidArg !== undefined + ? [ + `case " $original_args " in *" ${input.forbidArg} "*)`, + ` printf "%s\\n" "forbidden arg: ${input.forbidArg}" >&2`, + ` exit 9`, + "esac", + ] + : []), ...(input.requireImage ? [ 'if [ "$seen_image" != "1" ]; then', @@ -166,8 +185,12 @@ function withFakeCodexEnv( requireServiceTier?: string; requireReasoningEffort?: string; forbidReasoningEffort?: boolean; + requireArg?: string; + forbidArg?: string; stdinMustContain?: string; stdinMustNotContain?: string; + launchArgs?: string; + environment?: NodeJS.ProcessEnv; }, effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { @@ -175,8 +198,8 @@ function withFakeCodexEnv( const fs = yield* FileSystem.FileSystem; const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); const codexPath = yield* makeFakeCodexBinary(tempDir, input); - const config = decodeCodexSettings({ binaryPath: codexPath }); - const textGeneration = yield* makeCodexTextGeneration(config); + const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs }); + const textGeneration = yield* makeCodexTextGeneration(config, input.environment); return yield* effectFn(textGeneration); }).pipe(Effect.scoped); } @@ -237,6 +260,51 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { ), ); + it.effect("passes exec-safe launch args into codex exec", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "Add important change", + body: "", + }), + launchArgs: "--strict-config --listen off", + requireArg: "--strict-config", + forbidArg: "--listen", + }, + (textGeneration) => + textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/codex-effect", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md b/README.md", + modelSelection: DEFAULT_TEST_MODEL_SELECTION, + }), + ), + ); + + it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for codex exec over settings", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "Add important change", + body: "", + }), + launchArgs: "--enable settings-feature", + environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --listen off " }, + requireArg: "--strict-config", + forbidArg: "settings-feature", + }, + (textGeneration) => + textGeneration.generateCommitMessage({ + cwd: process.cwd(), + branch: "feature/codex-effect", + stagedSummary: "M README.md", + stagedPatch: "diff --git a/README.md b/README.md", + modelSelection: DEFAULT_TEST_MODEL_SELECTION, + }), + ), + ); + it.effect("defaults git text generation codex effort to low", () => withFakeCodexEnv( { diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 0e68994fd3d..6a5c0df43c7 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -14,6 +14,7 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { resolveAttachmentPath } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; +import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; import { TextGenerationError } from "@t3tools/contracts"; import * as TextGeneration from "./TextGeneration.ts"; import { @@ -174,6 +175,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func const outputPath = yield* writeTempFile(operation, "codex-output", ""); const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { + const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); const reasoningEffort = getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; @@ -182,6 +184,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func codexConfig.binaryPath || "codex", [ "exec", + ...codexExecLaunchArgs(launchArgs), "--ephemeral", "--skip-git-repo-check", "-s", diff --git a/apps/web/src/components/settings/ProviderSettingsForm.test.ts b/apps/web/src/components/settings/ProviderSettingsForm.test.ts index 33331c14901..ea8712a87eb 100644 --- a/apps/web/src/components/settings/ProviderSettingsForm.test.ts +++ b/apps/web/src/components/settings/ProviderSettingsForm.test.ts @@ -18,6 +18,7 @@ describe("ProviderSettingsForm helpers", () => { "binaryPath", "homePath", "shadowHomePath", + "launchArgs", ]); }); diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index ac2d47ca336..0f618729c43 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -141,6 +141,7 @@ describe("ServerSettingsPatch string normalization", () => { codex: { binaryPath: " /opt/homebrew/bin/codex ", homePath: " ~/.codex ", + launchArgs: " --strict-config --enable foo ", }, }, providerInstances: { @@ -157,6 +158,7 @@ describe("ServerSettingsPatch string normalization", () => { expect(patch.observability?.otlpTracesUrl).toBe("http://localhost:4318/v1/traces"); expect(patch.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex"); expect(patch.providers?.codex?.homePath).toBe("~/.codex"); + expect(patch.providers?.codex?.launchArgs).toBe("--strict-config --enable foo"); expect(patch.providerInstances?.[ProviderInstanceId.make("codex_personal")]?.driver).toBe( "codex", ); @@ -178,11 +180,13 @@ describe("ServerSettingsPatch string normalization", () => { codex: { ...defaultSettings.providers.codex, binaryPath: " /opt/homebrew/bin/codex ", + launchArgs: " --strict-config ", }, }, }); expect(encoded.addProjectBaseDirectory).toBe("~/Development"); expect(encoded.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex"); + expect(encoded.providers?.codex?.launchArgs).toBe("--strict-config"); }); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index b05f397bf5c..fe593f49199 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -191,13 +191,20 @@ export const CodexSettings = makeProviderSettingsSchema( }, }), ), + launchArgs: TrimmedString.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Launch arguments", + description: "Additional CLI arguments passed to codex app-server on session start.", + }), + ), customModels: Schema.Array(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { - order: ["binaryPath", "homePath", "shadowHomePath"], + order: ["binaryPath", "homePath", "shadowHomePath", "launchArgs"], }, ); export type CodexSettings = typeof CodexSettings.Type; @@ -469,6 +476,7 @@ const CodexSettingsPatch = Schema.Struct({ binaryPath: Schema.optionalKey(TrimmedString), homePath: Schema.optionalKey(TrimmedString), shadowHomePath: Schema.optionalKey(TrimmedString), + launchArgs: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(Schema.String)), }); diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts index adf2a3f03c0..1fc6ab0215e 100644 --- a/packages/shared/src/cliArgs.test.ts +++ b/packages/shared/src/cliArgs.test.ts @@ -1,6 +1,27 @@ import { describe, expect, it } from "vite-plus/test"; -import { parseCliArgs } from "./cliArgs.ts"; +import { parseCliArgs, tokenizeCliArgs } from "./cliArgs.ts"; + +describe("tokenizeCliArgs", () => { + it("preserves quoted values and escaped spaces", () => { + expect( + tokenizeCliArgs( + String.raw`--config model="gpt 5" --enable foo\ bar --config=profile='work profile'`, + ), + ).toEqual(["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"]); + }); + + it("preserves literal backslashes in path values", () => { + expect( + tokenizeCliArgs(String.raw`--config cacheDir=C:\Users\me --config "quoted=C:\Users\me"`), + ).toEqual([ + "--config", + String.raw`cacheDir=C:\Users\me`, + "--config", + String.raw`quoted=C:\Users\me`, + ]); + }); +}); describe("parseCliArgs", () => { it("returns empty result for empty string", () => { @@ -57,6 +78,13 @@ describe("parseCliArgs", () => { }); }); + it("parses quoted --append-system-prompt with value and --chrome", () => { + expect(parseCliArgs(`--append-system-prompt "always think step by step" --chrome`)).toEqual({ + flags: { "append-system-prompt": "always think step by step", chrome: null }, + positionals: [], + }); + }); + it("parses --max-budget-usd with numeric value", () => { expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({ flags: { chrome: null, "max-budget-usd": "5.00" }, diff --git a/packages/shared/src/cliArgs.ts b/packages/shared/src/cliArgs.ts index 20920093302..69851f2f3fb 100644 --- a/packages/shared/src/cliArgs.ts +++ b/packages/shared/src/cliArgs.ts @@ -7,10 +7,67 @@ export interface ParseCliArgsOptions { readonly booleanFlags?: readonly string[]; } +export function tokenizeCliArgs(args?: string): ReadonlyArray { + const input = args?.trim(); + if (!input) return []; + + const tokens: string[] = []; + let current = ""; + let quote: "'" | '"' | undefined; + let quoted = false; + + for (let index = 0; index < input.length; index++) { + const char = input[index]; + if (char === undefined) continue; + + if (quote) { + if (char === quote) { + quote = undefined; + quoted = true; + } else if (char === "\\" && quote === '"') { + const next = input[index + 1]; + if (next !== undefined && ['"', "\\", "$", "`"].includes(next)) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + continue; + } + + if (char === "'" || char === '"') { + quote = char; + quoted = true; + } else if (/\s/.test(char)) { + if (current || quoted) { + tokens.push(current); + current = ""; + quoted = false; + } + } else if (char === "\\") { + const next = input[index + 1]; + if (next !== undefined && /\s/.test(next)) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + } + + if (current || quoted) tokens.push(current); + return tokens; +} + /** * Parse CLI-style arguments into flags and positionals. * - * Accepts a string (split by whitespace) or a pre-split argv array. + * Accepts a string (quote-aware tokenized) or a pre-split argv array. * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax. * * parseCliArgs("") @@ -32,8 +89,7 @@ export function parseCliArgs( args: string | readonly string[], options?: ParseCliArgsOptions, ): ParsedCliArgs { - const tokens = - typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args); + const tokens = typeof args === "string" ? tokenizeCliArgs(args) : Array.from(args); const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined; const flags: Record = {}; From 501ce27b81826960dcbb9a93a6b3407efcf09366 Mon Sep 17 00:00:00 2001 From: Andrew Forster <76947376+Andrew-Forster@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:04:48 -0700 Subject: [PATCH 27/39] [orchestration] Clear stale active turn when session becomes inactive (#3159) Co-authored-by: Julius Marminge --- .../Layers/ProviderRuntimeIngestion.test.ts | 47 +++++++++++++++++++ .../Layers/ProviderRuntimeIngestion.ts | 20 +++++--- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index 72976768fec..20611e1ee75 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -448,6 +448,51 @@ describe("ProviderRuntimeIngestion", () => { expect(thread.session?.lastError).toBeNull(); }); + it("clears active turn when provider session becomes ready", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + + harness.emit({ + type: "turn.started", + eventId: asEventId("evt-turn-started-session-ready"), + provider: ProviderDriverKind.make("codex"), + createdAt: now, + threadId: asThreadId("thread-1"), + turnId: asTurnId("turn-session-ready"), + }); + + await waitForThread( + harness.readModel, + (thread) => + thread.session?.status === "running" && + thread.session?.activeTurnId === "turn-session-ready", + 10_000, + ); + + harness.emit({ + type: "session.state.changed", + eventId: asEventId("evt-session-state-ready-with-active-turn"), + provider: ProviderDriverKind.make("codex"), + threadId: asThreadId("thread-1"), + createdAt: "2026-01-01T00:00:01.000Z", + payload: { + state: "ready", + }, + }); + + const thread = await waitForThread( + harness.readModel, + (entry) => + entry.session?.status === "ready" && + entry.session?.activeTurnId === null && + entry.session?.lastError === null, + 10_000, + ); + expect(thread.session?.status).toBe("ready"); + expect(thread.session?.activeTurnId).toBeNull(); + expect(thread.session?.lastError).toBeNull(); + }); + it("does not clear active turn when session/thread started arrives mid-turn", async () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; @@ -466,6 +511,7 @@ describe("ProviderRuntimeIngestion", () => { (thread) => thread.session?.status === "running" && thread.session?.activeTurnId === "turn-midturn-lifecycle", + 10_000, ); harness.emit({ @@ -502,6 +548,7 @@ describe("ProviderRuntimeIngestion", () => { await waitForThread( harness.readModel, (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, + 10_000, ); }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index ef3454348e4..a5ec9d2b857 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -283,6 +283,12 @@ function orchestrationSessionStatusFromRuntimeState( } } +function sessionStatusAllowsActiveTurn( + status: ReturnType, +): boolean { + return status === "starting" || status === "running"; +} + function requestKindFromCanonicalRequestType( requestType: string | undefined, ): "command" | "file-read" | "file-change" | undefined { @@ -1362,12 +1368,6 @@ const make = Effect.gen(function* () { event.type === "turn.started" || event.type === "turn.completed" ) { - const nextActiveTurnId = - event.type === "turn.started" - ? (eventTurnId ?? null) - : event.type === "turn.completed" || event.type === "session.exited" - ? null - : activeTurnId; const status = (() => { switch (event.type) { case "session.state.changed": @@ -1387,6 +1387,14 @@ const make = Effect.gen(function* () { return activeTurnId !== null ? "running" : "ready"; } })(); + const nextActiveTurnId = + event.type === "turn.started" + ? (eventTurnId ?? null) + : event.type === "turn.completed" || event.type === "session.exited" + ? null + : event.type === "session.state.changed" && !sessionStatusAllowsActiveTurn(status) + ? null + : activeTurnId; const lastError = event.type === "session.state.changed" && event.payload.state === "error" ? (event.payload.reason ?? thread.session?.lastError ?? "Provider session error") From 08993a5ec8fbe5c55a2a4fa8f5fb37f870e87cf6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 20 Jul 2026 11:14:19 +0200 Subject: [PATCH 28/39] Regenerate Codex reset credit protocol bindings (#4173) Co-authored-by: codex --- .../scripts/generate.ts | 4 +- .../src/_generated/meta.gen.ts | 44 +- .../src/_generated/namespaces.gen.ts | 17 +- .../src/_generated/schema.gen.ts | 7141 ++++++++++++++--- .../src/protocol.test.ts | 86 + 5 files changed, 5973 insertions(+), 1319 deletions(-) diff --git a/packages/effect-codex-app-server/scripts/generate.ts b/packages/effect-codex-app-server/scripts/generate.ts index 3deb4514292..9f23a144524 100644 --- a/packages/effect-codex-app-server/scripts/generate.ts +++ b/packages/effect-codex-app-server/scripts/generate.ts @@ -17,7 +17,7 @@ import { } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -const UPSTREAM_REF = "b39f943a634a6e7ba86c3d6e8cf6d5f35e612566"; +const UPSTREAM_REF = "678157acaa819d5510adfe359abb5d0392cfe461"; const USER_AGENT = "effect-codex-app-server-generator"; const GITHUB_API_BASE = "https://api.github.com/repos/openai/codex/contents/codex-rs/app-server-protocol"; @@ -349,10 +349,12 @@ function resolveResponseTypeName( "account/logout": "LogoutAccountResponse", "account/rateLimits/read": "GetAccountRateLimitsResponse", "account/usage/read": "GetAccountTokenUsageResponse", + "account/workspaceMessages/read": "GetWorkspaceMessagesResponse", "config/batchWrite": "ConfigWriteResponse", "config/mcpServer/reload": "McpServerRefreshResponse", "config/value/write": "ConfigWriteResponse", "configRequirements/read": "ConfigRequirementsReadResponse", + "externalAgentConfig/import/readHistories": "ExternalAgentConfigImportHistoriesReadResponse", }; const override = overrides[method]; diff --git a/packages/effect-codex-app-server/src/_generated/meta.gen.ts b/packages/effect-codex-app-server/src/_generated/meta.gen.ts index ed39896bdb9..24452e881f6 100644 --- a/packages/effect-codex-app-server/src/_generated/meta.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/meta.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 678157acaa819d5510adfe359abb5d0392cfe461 import * as CodexSchema from "./schema.gen.ts"; @@ -40,7 +40,9 @@ export const CLIENT_REQUEST_METHODS = { "plugin/share/list": "plugin/share/list", "plugin/share/checkout": "plugin/share/checkout", "plugin/share/delete": "plugin/share/delete", + "app/read": "app/read", "app/list": "app/list", + "app/installed": "app/installed", "fs/readFile": "fs/readFile", "fs/writeFile": "fs/writeFile", "fs/createDirectory": "fs/createDirectory", @@ -73,7 +75,9 @@ export const CLIENT_REQUEST_METHODS = { "account/login/cancel": "account/login/cancel", "account/logout": "account/logout", "account/rateLimits/read": "account/rateLimits/read", + "account/rateLimitResetCredit/consume": "account/rateLimitResetCredit/consume", "account/usage/read": "account/usage/read", + "account/workspaceMessages/read": "account/workspaceMessages/read", "account/sendAddCreditsNudgeEmail": "account/sendAddCreditsNudgeEmail", "feedback/upload": "feedback/upload", "command/exec": "command/exec", @@ -83,6 +87,7 @@ export const CLIENT_REQUEST_METHODS = { "config/read": "config/read", "externalAgentConfig/detect": "externalAgentConfig/detect", "externalAgentConfig/import": "externalAgentConfig/import", + "externalAgentConfig/import/readHistories": "externalAgentConfig/import/readHistories", "config/value/write": "config/value/write", "config/batchWrite": "config/batchWrite", "configRequirements/read": "configRequirements/read", @@ -122,6 +127,8 @@ export const SERVER_NOTIFICATION_METHODS = { "thread/name/updated": "thread/name/updated", "thread/goal/updated": "thread/goal/updated", "thread/goal/cleared": "thread/goal/cleared", + "thread/environment/connected": "thread/environment/connected", + "thread/environment/disconnected": "thread/environment/disconnected", "thread/settings/updated": "thread/settings/updated", "thread/tokenUsage/updated": "thread/tokenUsage/updated", "turn/started": "turn/started", @@ -135,6 +142,7 @@ export const SERVER_NOTIFICATION_METHODS = { "item/autoApprovalReview/completed": "item/autoApprovalReview/completed", "item/completed": "item/completed", "rawResponseItem/completed": "rawResponseItem/completed", + "rawResponse/completed": "rawResponse/completed", "item/agentMessage/delta": "item/agentMessage/delta", "item/plan/delta": "item/plan/delta", "command/exec/outputDelta": "command/exec/outputDelta", @@ -152,6 +160,7 @@ export const SERVER_NOTIFICATION_METHODS = { "account/rateLimits/updated": "account/rateLimits/updated", "app/list/updated": "app/list/updated", "remoteControl/status/changed": "remoteControl/status/changed", + "externalAgentConfig/import/progress": "externalAgentConfig/import/progress", "externalAgentConfig/import/completed": "externalAgentConfig/import/completed", "fs/changed": "fs/changed", "item/reasoning/summaryTextDelta": "item/reasoning/summaryTextDelta", @@ -161,6 +170,7 @@ export const SERVER_NOTIFICATION_METHODS = { "model/rerouted": "model/rerouted", "model/verification": "model/verification", "turn/moderationMetadata": "turn/moderationMetadata", + "model/safetyBuffering/updated": "model/safetyBuffering/updated", warning: "warning", guardianWarning: "guardianWarning", deprecationNotice: "deprecationNotice", @@ -222,7 +232,9 @@ export interface ClientRequestParamsByMethod { readonly "plugin/share/list": CodexSchema.V2PluginShareListParams; readonly "plugin/share/checkout": CodexSchema.V2PluginShareCheckoutParams; readonly "plugin/share/delete": CodexSchema.V2PluginShareDeleteParams; + readonly "app/read": CodexSchema.V2AppsReadParams; readonly "app/list": CodexSchema.V2AppsListParams; + readonly "app/installed": CodexSchema.V2AppsInstalledParams; readonly "fs/readFile": CodexSchema.V2FsReadFileParams; readonly "fs/writeFile": CodexSchema.V2FsWriteFileParams; readonly "fs/createDirectory": CodexSchema.V2FsCreateDirectoryParams; @@ -255,7 +267,9 @@ export interface ClientRequestParamsByMethod { readonly "account/login/cancel": CodexSchema.V2CancelLoginAccountParams; readonly "account/logout": undefined; readonly "account/rateLimits/read": undefined; + readonly "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditParams; readonly "account/usage/read": undefined; + readonly "account/workspaceMessages/read": undefined; readonly "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailParams; readonly "feedback/upload": CodexSchema.V2FeedbackUploadParams; readonly "command/exec": CodexSchema.V2CommandExecParams; @@ -265,6 +279,7 @@ export interface ClientRequestParamsByMethod { readonly "config/read": CodexSchema.V2ConfigReadParams; readonly "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectParams; readonly "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportParams; + readonly "externalAgentConfig/import/readHistories": undefined; readonly "config/value/write": CodexSchema.V2ConfigValueWriteParams; readonly "config/batchWrite": CodexSchema.V2ConfigBatchWriteParams; readonly "configRequirements/read": undefined; @@ -312,7 +327,9 @@ export interface ClientRequestResponsesByMethod { readonly "plugin/share/list": CodexSchema.V2PluginShareListResponse; readonly "plugin/share/checkout": CodexSchema.V2PluginShareCheckoutResponse; readonly "plugin/share/delete": CodexSchema.V2PluginShareDeleteResponse; + readonly "app/read": CodexSchema.V2AppsReadResponse; readonly "app/list": CodexSchema.V2AppsListResponse; + readonly "app/installed": CodexSchema.V2AppsInstalledResponse; readonly "fs/readFile": CodexSchema.V2FsReadFileResponse; readonly "fs/writeFile": CodexSchema.V2FsWriteFileResponse; readonly "fs/createDirectory": CodexSchema.V2FsCreateDirectoryResponse; @@ -345,7 +362,9 @@ export interface ClientRequestResponsesByMethod { readonly "account/login/cancel": CodexSchema.V2CancelLoginAccountResponse; readonly "account/logout": CodexSchema.V2LogoutAccountResponse; readonly "account/rateLimits/read": CodexSchema.V2GetAccountRateLimitsResponse; + readonly "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse; readonly "account/usage/read": CodexSchema.V2GetAccountTokenUsageResponse; + readonly "account/workspaceMessages/read": CodexSchema.V2GetWorkspaceMessagesResponse; readonly "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailResponse; readonly "feedback/upload": CodexSchema.V2FeedbackUploadResponse; readonly "command/exec": CodexSchema.V2CommandExecResponse; @@ -355,6 +374,7 @@ export interface ClientRequestResponsesByMethod { readonly "config/read": CodexSchema.V2ConfigReadResponse; readonly "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectResponse; readonly "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportResponse; + readonly "externalAgentConfig/import/readHistories": CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse; readonly "config/value/write": CodexSchema.V2ConfigWriteResponse; readonly "config/batchWrite": CodexSchema.V2ConfigWriteResponse; readonly "configRequirements/read": CodexSchema.V2ConfigRequirementsReadResponse; @@ -407,6 +427,8 @@ export interface ServerNotificationParamsByMethod { readonly "thread/name/updated": CodexSchema.V2ThreadNameUpdatedNotification; readonly "thread/goal/updated": CodexSchema.V2ThreadGoalUpdatedNotification; readonly "thread/goal/cleared": CodexSchema.V2ThreadGoalClearedNotification; + readonly "thread/environment/connected": CodexSchema.V2EnvironmentConnectionNotification; + readonly "thread/environment/disconnected": CodexSchema.V2EnvironmentConnectionNotification; readonly "thread/settings/updated": CodexSchema.V2ThreadSettingsUpdatedNotification; readonly "thread/tokenUsage/updated": CodexSchema.V2ThreadTokenUsageUpdatedNotification; readonly "turn/started": CodexSchema.V2TurnStartedNotification; @@ -420,6 +442,7 @@ export interface ServerNotificationParamsByMethod { readonly "item/autoApprovalReview/completed": CodexSchema.V2ItemGuardianApprovalReviewCompletedNotification; readonly "item/completed": CodexSchema.V2ItemCompletedNotification; readonly "rawResponseItem/completed": CodexSchema.V2RawResponseItemCompletedNotification; + readonly "rawResponse/completed": CodexSchema.V2RawResponseCompletedNotification; readonly "item/agentMessage/delta": CodexSchema.V2AgentMessageDeltaNotification; readonly "item/plan/delta": CodexSchema.V2PlanDeltaNotification; readonly "command/exec/outputDelta": CodexSchema.V2CommandExecOutputDeltaNotification; @@ -437,6 +460,7 @@ export interface ServerNotificationParamsByMethod { readonly "account/rateLimits/updated": CodexSchema.V2AccountRateLimitsUpdatedNotification; readonly "app/list/updated": CodexSchema.V2AppListUpdatedNotification; readonly "remoteControl/status/changed": CodexSchema.V2RemoteControlStatusChangedNotification; + readonly "externalAgentConfig/import/progress": CodexSchema.V2ExternalAgentConfigImportProgressNotification; readonly "externalAgentConfig/import/completed": CodexSchema.V2ExternalAgentConfigImportCompletedNotification; readonly "fs/changed": CodexSchema.V2FsChangedNotification; readonly "item/reasoning/summaryTextDelta": CodexSchema.V2ReasoningSummaryTextDeltaNotification; @@ -446,6 +470,7 @@ export interface ServerNotificationParamsByMethod { readonly "model/rerouted": CodexSchema.V2ModelReroutedNotification; readonly "model/verification": CodexSchema.V2ModelVerificationNotification; readonly "turn/moderationMetadata": CodexSchema.V2TurnModerationMetadataNotification; + readonly "model/safetyBuffering/updated": CodexSchema.V2ModelSafetyBufferingUpdatedNotification; readonly warning: CodexSchema.V2WarningNotification; readonly guardianWarning: CodexSchema.V2GuardianWarningNotification; readonly deprecationNotice: CodexSchema.V2DeprecationNoticeNotification; @@ -502,7 +527,9 @@ export const CLIENT_REQUEST_PARAMS = { "plugin/share/list": CodexSchema.V2PluginShareListParams, "plugin/share/checkout": CodexSchema.V2PluginShareCheckoutParams, "plugin/share/delete": CodexSchema.V2PluginShareDeleteParams, + "app/read": CodexSchema.V2AppsReadParams, "app/list": CodexSchema.V2AppsListParams, + "app/installed": CodexSchema.V2AppsInstalledParams, "fs/readFile": CodexSchema.V2FsReadFileParams, "fs/writeFile": CodexSchema.V2FsWriteFileParams, "fs/createDirectory": CodexSchema.V2FsCreateDirectoryParams, @@ -535,7 +562,9 @@ export const CLIENT_REQUEST_PARAMS = { "account/login/cancel": CodexSchema.V2CancelLoginAccountParams, "account/logout": undefined, "account/rateLimits/read": undefined, + "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditParams, "account/usage/read": undefined, + "account/workspaceMessages/read": undefined, "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailParams, "feedback/upload": CodexSchema.V2FeedbackUploadParams, "command/exec": CodexSchema.V2CommandExecParams, @@ -545,6 +574,7 @@ export const CLIENT_REQUEST_PARAMS = { "config/read": CodexSchema.V2ConfigReadParams, "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectParams, "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportParams, + "externalAgentConfig/import/readHistories": undefined, "config/value/write": CodexSchema.V2ConfigValueWriteParams, "config/batchWrite": CodexSchema.V2ConfigBatchWriteParams, "configRequirements/read": undefined, @@ -592,7 +622,9 @@ export const CLIENT_REQUEST_RESPONSES = { "plugin/share/list": CodexSchema.V2PluginShareListResponse, "plugin/share/checkout": CodexSchema.V2PluginShareCheckoutResponse, "plugin/share/delete": CodexSchema.V2PluginShareDeleteResponse, + "app/read": CodexSchema.V2AppsReadResponse, "app/list": CodexSchema.V2AppsListResponse, + "app/installed": CodexSchema.V2AppsInstalledResponse, "fs/readFile": CodexSchema.V2FsReadFileResponse, "fs/writeFile": CodexSchema.V2FsWriteFileResponse, "fs/createDirectory": CodexSchema.V2FsCreateDirectoryResponse, @@ -625,7 +657,9 @@ export const CLIENT_REQUEST_RESPONSES = { "account/login/cancel": CodexSchema.V2CancelLoginAccountResponse, "account/logout": CodexSchema.V2LogoutAccountResponse, "account/rateLimits/read": CodexSchema.V2GetAccountRateLimitsResponse, + "account/rateLimitResetCredit/consume": CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse, "account/usage/read": CodexSchema.V2GetAccountTokenUsageResponse, + "account/workspaceMessages/read": CodexSchema.V2GetWorkspaceMessagesResponse, "account/sendAddCreditsNudgeEmail": CodexSchema.V2SendAddCreditsNudgeEmailResponse, "feedback/upload": CodexSchema.V2FeedbackUploadResponse, "command/exec": CodexSchema.V2CommandExecResponse, @@ -635,6 +669,8 @@ export const CLIENT_REQUEST_RESPONSES = { "config/read": CodexSchema.V2ConfigReadResponse, "externalAgentConfig/detect": CodexSchema.V2ExternalAgentConfigDetectResponse, "externalAgentConfig/import": CodexSchema.V2ExternalAgentConfigImportResponse, + "externalAgentConfig/import/readHistories": + CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse, "config/value/write": CodexSchema.V2ConfigWriteResponse, "config/batchWrite": CodexSchema.V2ConfigWriteResponse, "configRequirements/read": CodexSchema.V2ConfigRequirementsReadResponse, @@ -687,6 +723,8 @@ export const SERVER_NOTIFICATION_PARAMS = { "thread/name/updated": CodexSchema.V2ThreadNameUpdatedNotification, "thread/goal/updated": CodexSchema.V2ThreadGoalUpdatedNotification, "thread/goal/cleared": CodexSchema.V2ThreadGoalClearedNotification, + "thread/environment/connected": CodexSchema.V2EnvironmentConnectionNotification, + "thread/environment/disconnected": CodexSchema.V2EnvironmentConnectionNotification, "thread/settings/updated": CodexSchema.V2ThreadSettingsUpdatedNotification, "thread/tokenUsage/updated": CodexSchema.V2ThreadTokenUsageUpdatedNotification, "turn/started": CodexSchema.V2TurnStartedNotification, @@ -701,6 +739,7 @@ export const SERVER_NOTIFICATION_PARAMS = { CodexSchema.V2ItemGuardianApprovalReviewCompletedNotification, "item/completed": CodexSchema.V2ItemCompletedNotification, "rawResponseItem/completed": CodexSchema.V2RawResponseItemCompletedNotification, + "rawResponse/completed": CodexSchema.V2RawResponseCompletedNotification, "item/agentMessage/delta": CodexSchema.V2AgentMessageDeltaNotification, "item/plan/delta": CodexSchema.V2PlanDeltaNotification, "command/exec/outputDelta": CodexSchema.V2CommandExecOutputDeltaNotification, @@ -718,6 +757,8 @@ export const SERVER_NOTIFICATION_PARAMS = { "account/rateLimits/updated": CodexSchema.V2AccountRateLimitsUpdatedNotification, "app/list/updated": CodexSchema.V2AppListUpdatedNotification, "remoteControl/status/changed": CodexSchema.V2RemoteControlStatusChangedNotification, + "externalAgentConfig/import/progress": + CodexSchema.V2ExternalAgentConfigImportProgressNotification, "externalAgentConfig/import/completed": CodexSchema.V2ExternalAgentConfigImportCompletedNotification, "fs/changed": CodexSchema.V2FsChangedNotification, @@ -728,6 +769,7 @@ export const SERVER_NOTIFICATION_PARAMS = { "model/rerouted": CodexSchema.V2ModelReroutedNotification, "model/verification": CodexSchema.V2ModelVerificationNotification, "turn/moderationMetadata": CodexSchema.V2TurnModerationMetadataNotification, + "model/safetyBuffering/updated": CodexSchema.V2ModelSafetyBufferingUpdatedNotification, warning: CodexSchema.V2WarningNotification, guardianWarning: CodexSchema.V2GuardianWarningNotification, deprecationNotice: CodexSchema.V2DeprecationNoticeNotification, diff --git a/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts b/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts index 74154a2769f..8665ad6f436 100644 --- a/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/namespaces.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 678157acaa819d5510adfe359abb5d0392cfe461 import * as CodexSchema from "./schema.gen.ts"; @@ -14,8 +14,12 @@ export const v2 = { AccountUpdatedNotification: CodexSchema.V2AccountUpdatedNotification, AgentMessageDeltaNotification: CodexSchema.V2AgentMessageDeltaNotification, AppListUpdatedNotification: CodexSchema.V2AppListUpdatedNotification, + AppsInstalledParams: CodexSchema.V2AppsInstalledParams, + AppsInstalledResponse: CodexSchema.V2AppsInstalledResponse, AppsListParams: CodexSchema.V2AppsListParams, AppsListResponse: CodexSchema.V2AppsListResponse, + AppsReadParams: CodexSchema.V2AppsReadParams, + AppsReadResponse: CodexSchema.V2AppsReadResponse, CancelLoginAccountParams: CodexSchema.V2CancelLoginAccountParams, CancelLoginAccountResponse: CodexSchema.V2CancelLoginAccountResponse, CommandExecOutputDeltaNotification: CodexSchema.V2CommandExecOutputDeltaNotification, @@ -35,8 +39,12 @@ export const v2 = { ConfigValueWriteParams: CodexSchema.V2ConfigValueWriteParams, ConfigWarningNotification: CodexSchema.V2ConfigWarningNotification, ConfigWriteResponse: CodexSchema.V2ConfigWriteResponse, + ConsumeAccountRateLimitResetCreditParams: CodexSchema.V2ConsumeAccountRateLimitResetCreditParams, + ConsumeAccountRateLimitResetCreditResponse: + CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse, ContextCompactedNotification: CodexSchema.V2ContextCompactedNotification, DeprecationNoticeNotification: CodexSchema.V2DeprecationNoticeNotification, + EnvironmentConnectionNotification: CodexSchema.V2EnvironmentConnectionNotification, ErrorNotification: CodexSchema.V2ErrorNotification, ExperimentalFeatureEnablementSetParams: CodexSchema.V2ExperimentalFeatureEnablementSetParams, ExperimentalFeatureEnablementSetResponse: CodexSchema.V2ExperimentalFeatureEnablementSetResponse, @@ -46,7 +54,11 @@ export const v2 = { ExternalAgentConfigDetectResponse: CodexSchema.V2ExternalAgentConfigDetectResponse, ExternalAgentConfigImportCompletedNotification: CodexSchema.V2ExternalAgentConfigImportCompletedNotification, + ExternalAgentConfigImportHistoriesReadResponse: + CodexSchema.V2ExternalAgentConfigImportHistoriesReadResponse, ExternalAgentConfigImportParams: CodexSchema.V2ExternalAgentConfigImportParams, + ExternalAgentConfigImportProgressNotification: + CodexSchema.V2ExternalAgentConfigImportProgressNotification, ExternalAgentConfigImportResponse: CodexSchema.V2ExternalAgentConfigImportResponse, FeedbackUploadParams: CodexSchema.V2FeedbackUploadParams, FeedbackUploadResponse: CodexSchema.V2FeedbackUploadResponse, @@ -75,6 +87,7 @@ export const v2 = { GetAccountRateLimitsResponse: CodexSchema.V2GetAccountRateLimitsResponse, GetAccountResponse: CodexSchema.V2GetAccountResponse, GetAccountTokenUsageResponse: CodexSchema.V2GetAccountTokenUsageResponse, + GetWorkspaceMessagesResponse: CodexSchema.V2GetWorkspaceMessagesResponse, GuardianWarningNotification: CodexSchema.V2GuardianWarningNotification, HookCompletedNotification: CodexSchema.V2HookCompletedNotification, HooksListParams: CodexSchema.V2HooksListParams, @@ -112,6 +125,7 @@ export const v2 = { ModelProviderCapabilitiesReadParams: CodexSchema.V2ModelProviderCapabilitiesReadParams, ModelProviderCapabilitiesReadResponse: CodexSchema.V2ModelProviderCapabilitiesReadResponse, ModelReroutedNotification: CodexSchema.V2ModelReroutedNotification, + ModelSafetyBufferingUpdatedNotification: CodexSchema.V2ModelSafetyBufferingUpdatedNotification, ModelVerificationNotification: CodexSchema.V2ModelVerificationNotification, PermissionProfileListParams: CodexSchema.V2PermissionProfileListParams, PermissionProfileListResponse: CodexSchema.V2PermissionProfileListResponse, @@ -140,6 +154,7 @@ export const v2 = { PluginUninstallResponse: CodexSchema.V2PluginUninstallResponse, ProcessExitedNotification: CodexSchema.V2ProcessExitedNotification, ProcessOutputDeltaNotification: CodexSchema.V2ProcessOutputDeltaNotification, + RawResponseCompletedNotification: CodexSchema.V2RawResponseCompletedNotification, RawResponseItemCompletedNotification: CodexSchema.V2RawResponseItemCompletedNotification, ReasoningSummaryPartAddedNotification: CodexSchema.V2ReasoningSummaryPartAddedNotification, ReasoningSummaryTextDeltaNotification: CodexSchema.V2ReasoningSummaryTextDeltaNotification, diff --git a/packages/effect-codex-app-server/src/_generated/schema.gen.ts b/packages/effect-codex-app-server/src/_generated/schema.gen.ts index fb93292384d..d826df60f19 100644 --- a/packages/effect-codex-app-server/src/_generated/schema.gen.ts +++ b/packages/effect-codex-app-server/src/_generated/schema.gen.ts @@ -1,5 +1,5 @@ // This file is generated by the effect-codex-app-server package. Do not edit manually. -// Upstream protocol ref: b39f943a634a6e7ba86c3d6e8cf6d5f35e612566 +// Upstream protocol ref: 678157acaa819d5510adfe359abb5d0392cfe461 import * as Schema from "effect/Schema"; @@ -51,12 +51,17 @@ export const ClientRequest__AddCreditsNudgeCreditType = Schema.Literals(["credit export type ClientRequest__AdditionalContextKind = "untrusted" | "application"; export const ClientRequest__AdditionalContextKind = Schema.Literals(["untrusted", "application"]); -export type ClientRequest__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; -}; +export type ClientRequest__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const ClientRequest__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -77,6 +82,27 @@ export const ClientRequest__ApprovalsReviewer = Schema.Literals([ "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", }); +export type ClientRequest__AppsInstalledParams = { + readonly forceRefresh?: boolean; + readonly threadId?: string | null; +}; +export const ClientRequest__AppsInstalledParams = Schema.Struct({ + forceRefresh: Schema.optionalKey( + Schema.Boolean.annotate({ + description: + "When true and Apps are permitted, refresh and publish the hosted connector runtime tool snapshot first.", + }), + ), + threadId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Optional loaded thread id used to evaluate effective app configuration.", + }), + Schema.Null, + ]), + ), +}).annotate({ description: "Read the committed installed connector runtime snapshot." }); + export type ClientRequest__AppsListParams = { readonly cursor?: string | null; readonly forceRefetch?: boolean; @@ -119,9 +145,25 @@ export const ClientRequest__AppsListParams = Schema.Struct({ ), }).annotate({ description: "EXPERIMENTAL - list available apps/connectors." }); +export type ClientRequest__AppsReadParams = { + readonly appIds: ReadonlyArray; + readonly includeTools?: boolean; +}; +export const ClientRequest__AppsReadParams = Schema.Struct({ + appIds: Schema.Array(Schema.String).annotate({ + description: + "App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while preserving their first-request order.", + }), + includeTools: Schema.optionalKey( + Schema.Boolean.annotate({ + description: + "When true, include display-only public tool summaries in the returned metadata.", + }), + ), +}).annotate({ description: "EXPERIMENTAL - read metadata for specific apps/connectors." }); + export type ClientRequest__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -135,7 +177,7 @@ export type ClientRequest__AskForApproval = }; export const ClientRequest__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -260,6 +302,53 @@ export const ClientRequest__ConfigReadParams = Schema.Struct({ includeLayers: Schema.optionalKey(Schema.Boolean), }); +export type ClientRequest__ConsumeAccountRateLimitResetCreditParams = { + readonly creditId?: string | null; + readonly idempotencyKey: string; +}; +export const ClientRequest__ConsumeAccountRateLimitResetCreditParams = Schema.Struct({ + creditId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + }), + Schema.Null, + ]), + ), + idempotencyKey: Schema.String.annotate({ + description: + "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + }), +}); + +export type ClientRequest__ConversationTextRole = "user" | "developer" | "assistant"; +export const ClientRequest__ConversationTextRole = Schema.Literals([ + "user", + "developer", + "assistant", +]); + +export type ClientRequest__DynamicToolNamespaceTool = { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; +}; +export const ClientRequest__DynamicToolNamespaceTool = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolNamespaceToolType" }), + }).annotate({ title: "FunctionDynamicToolNamespaceTool" }), + ], + { mode: "oneOf" }, +); + export type ClientRequest__ExperimentalFeatureEnablementSetParams = { readonly enablement: { readonly [x: string]: boolean }; }; @@ -309,6 +398,8 @@ export const ClientRequest__ExperimentalFeatureListParams = Schema.Struct({ export type ClientRequest__ExternalAgentConfigDetectParams = { readonly cwds?: ReadonlyArray | null; readonly includeHome?: boolean; + readonly migrationSource?: string | null; + readonly source?: string | null; }; export const ClientRequest__ExternalAgentConfigDetectParams = Schema.Struct({ cwds: Schema.optionalKey( @@ -321,9 +412,27 @@ export const ClientRequest__ExternalAgentConfigDetectParams = Schema.Struct({ ), includeHome: Schema.optionalKey( Schema.Boolean.annotate({ - description: "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + description: "If true, include detection under the user's home directory.", }), ), + migrationSource: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional migration-source selector. Missing or unrecognized values use the default source.", + }), + Schema.Null, + ]), + ), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Deprecated field retained for compatibility. This field is ignored; use `migrationSource` to select the migration source.", + }), + Schema.Null, + ]), + ), }); export type ClientRequest__ExternalAgentConfigMigrationItemType = @@ -335,6 +444,7 @@ export type ClientRequest__ExternalAgentConfigMigrationItemType = | "SUBAGENTS" | "HOOKS" | "COMMANDS" + | "MEMORY" | "SESSIONS"; export const ClientRequest__ExternalAgentConfigMigrationItemType = Schema.Literals([ "AGENTS_MD", @@ -345,6 +455,7 @@ export const ClientRequest__ExternalAgentConfigMigrationItemType = Schema.Litera "SUBAGENTS", "HOOKS", "COMMANDS", + "MEMORY", "SESSIONS", ]); @@ -527,6 +638,7 @@ export const ClientRequest__ImageDetail = Schema.Literals(["auto", "low", "high" export type ClientRequest__InitializeCapabilities = { readonly experimentalApi?: boolean; + readonly mcpServerOpenaiFormElicitation?: boolean; readonly optOutNotificationMethods?: ReadonlyArray | null; readonly requestAttestation?: boolean; }; @@ -537,6 +649,11 @@ export const ClientRequest__InitializeCapabilities = Schema.Struct({ default: false, }), ), + mcpServerOpenaiFormElicitation: Schema.optionalKey( + Schema.Boolean.annotate({ + description: "Allow downstream MCP servers to request OpenAI extended form elicitations.", + }), + ), optOutNotificationMethods: Schema.optionalKey( Schema.Union([ Schema.Array(Schema.String).annotate({ @@ -554,6 +671,19 @@ export const ClientRequest__InitializeCapabilities = Schema.Struct({ ), }).annotate({ description: "Client-declared capabilities negotiated during initialize." }); +export type ClientRequest__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const ClientRequest__InternalChatMessageMetadataPassthrough = Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", +}); + +export type ClientRequest__LegacyAppPathString = string; +export const ClientRequest__LegacyAppPathString = Schema.String; + export type ClientRequest__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -592,59 +722,8 @@ export const ClientRequest__LocalShellStatus = Schema.Literals([ "incomplete", ]); -export type ClientRequest__LoginAccountParams = - | { readonly apiKey: string; readonly type: "apiKey" } - | { readonly codexStreamlinedLogin?: boolean; readonly type: "chatgpt" } - | { readonly type: "chatgptDeviceCode" } - | { - readonly accessToken: string; - readonly chatgptAccountId: string; - readonly chatgptPlanType?: string | null; - readonly type: "chatgptAuthTokens"; - }; -export const ClientRequest__LoginAccountParams = Schema.Union( - [ - Schema.Struct({ - apiKey: Schema.String, - type: Schema.Literal("apiKey").annotate({ title: "ApiKeyLoginAccountParamsType" }), - }).annotate({ title: "ApiKeyLoginAccountParams" }), - Schema.Struct({ - codexStreamlinedLogin: Schema.optionalKey(Schema.Boolean), - type: Schema.Literal("chatgpt").annotate({ title: "ChatgptLoginAccountParamsType" }), - }).annotate({ title: "ChatgptLoginAccountParams" }), - Schema.Struct({ - type: Schema.Literal("chatgptDeviceCode").annotate({ - title: "ChatgptDeviceCodeLoginAccountParamsType", - }), - }).annotate({ title: "ChatgptDeviceCodeLoginAccountParams" }), - Schema.Struct({ - accessToken: Schema.String.annotate({ - description: - "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", - }), - chatgptAccountId: Schema.String.annotate({ - description: "Workspace/account identifier supplied by the client.", - }), - chatgptPlanType: Schema.optionalKey( - Schema.Union([ - Schema.String.annotate({ - description: - "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", - }), - Schema.Null, - ]), - ), - type: Schema.Literal("chatgptAuthTokens").annotate({ - title: "ChatgptAuthTokensLoginAccountParamsType", - }), - }).annotate({ - title: "ChatgptAuthTokensLoginAccountParams", - description: - "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", - }), - ], - { mode: "oneOf" }, -); +export type ClientRequest__LoginAppBrand = "codex" | "chatgpt"; +export const ClientRequest__LoginAppBrand = Schema.Literals(["codex", "chatgpt"]); export type ClientRequest__MarketplaceAddParams = { readonly refName?: string | null; @@ -684,11 +763,13 @@ export const ClientRequest__McpServerMigration = Schema.Struct({ name: Schema.St export type ClientRequest__McpServerOauthLoginParams = { readonly name: string; readonly scopes?: ReadonlyArray | null; + readonly threadId?: string | null; readonly timeoutSecs?: number | null; }; export const ClientRequest__McpServerOauthLoginParams = Schema.Struct({ name: Schema.String, scopes: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), timeoutSecs: Schema.optionalKey( Schema.Union([Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), Schema.Null]), ), @@ -808,12 +889,14 @@ export type ClientRequest__PluginListMarketplaceKind = | "local" | "vertical" | "workspace-directory" - | "shared-with-me"; + | "shared-with-me" + | "created-by-me-remote"; export const ClientRequest__PluginListMarketplaceKind = Schema.Literals([ "local", "vertical", "workspace-directory", "shared-with-me", + "created-by-me-remote", ]); export type ClientRequest__PluginShareCheckoutParams = { readonly remotePluginId: string }; @@ -846,10 +929,11 @@ export const ClientRequest__PluginSharePrincipalType = Schema.Literals([ export type ClientRequest__PluginShareTargetRole = "reader" | "editor"; export const ClientRequest__PluginShareTargetRole = Schema.Literals(["reader", "editor"]); -export type ClientRequest__PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE"; +export type ClientRequest__PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE" | "LISTED"; export const ClientRequest__PluginShareUpdateDiscoverability = Schema.Literals([ "UNLISTED", "PRIVATE", + "LISTED", ]); export type ClientRequest__PluginSkillReadParams = { @@ -1042,6 +1126,9 @@ export const ClientRequest__SessionMigration = Schema.Struct({ title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ClientRequest__SkillMigration = { readonly name: string }; +export const ClientRequest__SkillMigration = Schema.Struct({ name: Schema.String }); + export type ClientRequest__SkillsListParams = { readonly cwds?: ReadonlyArray; readonly forceReload?: boolean; @@ -1236,7 +1323,7 @@ export const ClientRequest__ThreadRollbackParams = Schema.Struct({ .check(Schema.isInt()) .check(Schema.isGreaterThanOrEqualTo(0)), threadId: Schema.String, -}); +}).annotate({ description: "DEPRECATED: `thread/rollback` will be removed soon." }); export type ClientRequest__ThreadSetNameParams = { readonly name: string; @@ -1259,8 +1346,12 @@ export const ClientRequest__ThreadShellCommandParams = Schema.Struct({ threadId: Schema.String, }); -export type ClientRequest__ThreadSortKey = "created_at" | "updated_at"; -export const ClientRequest__ThreadSortKey = Schema.Literals(["created_at", "updated_at"]); +export type ClientRequest__ThreadSortKey = "created_at" | "updated_at" | "recency_at"; +export const ClientRequest__ThreadSortKey = Schema.Literals([ + "created_at", + "updated_at", + "recency_at", +]); export type ClientRequest__ThreadSource = string; export const ClientRequest__ThreadSource = Schema.String; @@ -1333,39 +1424,8 @@ export const CommandExecutionRequestApprovalParams__FileSystemAccessMode = Schem "deny", ]); -export type CommandExecutionRequestApprovalParams__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const CommandExecutionRequestApprovalParams__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); +export type CommandExecutionRequestApprovalParams__LegacyAppPathString = string; +export const CommandExecutionRequestApprovalParams__LegacyAppPathString = Schema.String; export type CommandExecutionRequestApprovalParams__NetworkApprovalProtocol = | "http" @@ -1393,7 +1453,8 @@ export const CommandExecutionRequestApprovalResponse__NetworkPolicyRuleAction = export type DynamicToolCallResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const DynamicToolCallResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -1408,6 +1469,12 @@ export const DynamicToolCallResponse__DynamicToolCallOutputContentItem = Schema. title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -1630,45 +1697,8 @@ export const PermissionsRequestApprovalParams__FileSystemAccessMode = Schema.Lit "deny", ]); -export type PermissionsRequestApprovalParams__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const PermissionsRequestApprovalParams__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); - -export type PermissionsRequestApprovalResponse__AbsolutePathBuf = string; -export const PermissionsRequestApprovalResponse__AbsolutePathBuf = Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", -}); +export type PermissionsRequestApprovalParams__LegacyAppPathString = string; +export const PermissionsRequestApprovalParams__LegacyAppPathString = Schema.String; export type PermissionsRequestApprovalResponse__AdditionalNetworkPermissions = { readonly enabled?: boolean | null; @@ -1684,39 +1714,8 @@ export const PermissionsRequestApprovalResponse__FileSystemAccessMode = Schema.L "deny", ]); -export type PermissionsRequestApprovalResponse__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const PermissionsRequestApprovalResponse__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); +export type PermissionsRequestApprovalResponse__LegacyAppPathString = string; +export const PermissionsRequestApprovalResponse__LegacyAppPathString = Schema.String; export type ServerNotification__AbsolutePathBuf = string; export const ServerNotification__AbsolutePathBuf = Schema.String.annotate({ @@ -1821,7 +1820,6 @@ export const ServerNotification__ApprovalsReviewer = Schema.Literals([ export type ServerNotification__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -1835,7 +1833,7 @@ export type ServerNotification__AskForApproval = }; export const ServerNotification__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -1853,14 +1851,18 @@ export type ServerNotification__AuthMode = | "apikey" | "chatgpt" | "chatgptAuthTokens" + | "headers" | "agentIdentity" - | "personalAccessToken"; + | "personalAccessToken" + | "bedrockApiKey"; export const ServerNotification__AuthMode = Schema.Literals([ "apikey", "chatgpt", "chatgptAuthTokens", + "headers", "agentIdentity", "personalAccessToken", + "bedrockApiKey", ]).annotate({ description: "Authentication mode for OpenAI-backed providers." }); export type ServerNotification__AutoReviewDecisionSource = "agent"; @@ -1973,7 +1975,8 @@ export const ServerNotification__DeprecationNoticeNotification = Schema.Struct({ export type ServerNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const ServerNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -1988,6 +1991,12 @@ export const ServerNotification__DynamicToolCallOutputContentItem = Schema.Union title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -1999,8 +2008,38 @@ export const ServerNotification__DynamicToolCallStatus = Schema.Literals([ "failed", ]); -export type ServerNotification__ExternalAgentConfigImportCompletedNotification = {}; -export const ServerNotification__ExternalAgentConfigImportCompletedNotification = Schema.Struct({}); +export type ServerNotification__EnvironmentConnectionNotification = { + readonly environmentId: string; + readonly threadId: string; +}; +export const ServerNotification__EnvironmentConnectionNotification = Schema.Struct({ + environmentId: Schema.String, + threadId: Schema.String, +}); + +export type ServerNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; +export const ServerNotification__ExternalAgentConfigMigrationItemType = Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS", +]); export type ServerNotification__FileChangeOutputDeltaNotification = { readonly delta: string; @@ -2021,40 +2060,6 @@ export const ServerNotification__FileChangeOutputDeltaNotification = Schema.Stru export type ServerNotification__FileSystemAccessMode = "read" | "write" | "deny"; export const ServerNotification__FileSystemAccessMode = Schema.Literals(["read", "write", "deny"]); -export type ServerNotification__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const ServerNotification__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); - export type ServerNotification__FuzzyFileSearchMatchType = "file" | "directory"; export const ServerNotification__FuzzyFileSearchMatchType = Schema.Literals(["file", "directory"]); @@ -2127,6 +2132,7 @@ export type ServerNotification__HookEventName = | "preCompact" | "postCompact" | "sessionStart" + | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" @@ -2138,6 +2144,7 @@ export const ServerNotification__HookEventName = Schema.Literals([ "preCompact", "postCompact", "sessionStart", + "sessionEnd", "userPromptSubmit", "subagentStart", "subagentStop", @@ -2193,17 +2200,27 @@ export const ServerNotification__HookScope = Schema.Literals(["thread", "turn"]) export type ServerNotification__ImageDetail = "auto" | "low" | "high" | "original"; export const ServerNotification__ImageDetail = Schema.Literals(["auto", "low", "high", "original"]); +export type ServerNotification__LegacyAppPathString = string; +export const ServerNotification__LegacyAppPathString = Schema.String; + export type ServerNotification__McpServerOauthLoginCompletedNotification = { readonly error?: string | null; readonly name: string; readonly success: boolean; + readonly threadId?: string | null; }; export const ServerNotification__McpServerOauthLoginCompletedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, success: Schema.Boolean, + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ServerNotification__McpServerStartupFailureReason = "reauthenticationRequired"; +export const ServerNotification__McpServerStartupFailureReason = Schema.Literal( + "reauthenticationRequired", +); + export type ServerNotification__McpServerStartupState = | "starting" | "ready" @@ -2216,6 +2233,21 @@ export const ServerNotification__McpServerStartupState = Schema.Literals([ "cancelled", ]); +export type ServerNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const ServerNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type ServerNotification__McpToolCallError = { readonly message: string }; export const ServerNotification__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -2284,6 +2316,25 @@ export const ServerNotification__ModeKind = Schema.Literals(["plan", "default"]) export type ServerNotification__ModelRerouteReason = "highRiskCyberActivity"; export const ServerNotification__ModelRerouteReason = Schema.Literal("highRiskCyberActivity"); +export type ServerNotification__ModelSafetyBufferingUpdatedNotification = { + readonly fasterModel?: string | null; + readonly model: string; + readonly reasons: ReadonlyArray; + readonly showBufferingUi: boolean; + readonly threadId: string; + readonly turnId: string; + readonly useCases: ReadonlyArray; +}; +export const ServerNotification__ModelSafetyBufferingUpdatedNotification = Schema.Struct({ + fasterModel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + model: Schema.String, + reasons: Schema.Array(Schema.String), + showBufferingUi: Schema.Boolean, + threadId: Schema.String, + turnId: Schema.String, + useCases: Schema.Array(Schema.String), +}); + export type ServerNotification__ModelVerification = "trustedAccessForCyber"; export const ServerNotification__ModelVerification = Schema.Literal("trustedAccessForCyber"); @@ -2465,8 +2516,8 @@ export const ServerNotification__RateLimitWindow = Schema.Struct({ ), }); -export type ServerNotification__RealtimeConversationVersion = "v1" | "v2"; -export const ServerNotification__RealtimeConversationVersion = Schema.Literals(["v1", "v2"]); +export type ServerNotification__RealtimeConversationVersion = "v1" | "v2" | "v3"; +export const ServerNotification__RealtimeConversationVersion = Schema.Literals(["v1", "v2", "v3"]); export type ServerNotification__ReasoningEffort = string; export const ServerNotification__ReasoningEffort = Schema.String.annotate({ @@ -2782,6 +2833,7 @@ export const ServerNotification__ThreadUnarchivedNotification = Schema.Struct({ }); export type ServerNotification__TokenUsageBreakdown = { + readonly cacheWriteInputTokens?: number; readonly cachedInputTokens: number; readonly inputTokens: number; readonly outputTokens: number; @@ -2789,6 +2841,9 @@ export type ServerNotification__TokenUsageBreakdown = { readonly totalTokens: number; }; export const ServerNotification__TokenUsageBreakdown = Schema.Struct({ + cacheWriteInputTokens: Schema.optionalKey( + Schema.Number.annotate({ default: 0, format: "int64" }).check(Schema.isInt()), + ), cachedInputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), inputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), outputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), @@ -2998,39 +3053,8 @@ export const ServerRequest__FileChangeRequestApprovalParams = Schema.Struct({ export type ServerRequest__FileSystemAccessMode = "read" | "write" | "deny"; export const ServerRequest__FileSystemAccessMode = Schema.Literals(["read", "write", "deny"]); -export type ServerRequest__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const ServerRequest__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); +export type ServerRequest__LegacyAppPathString = string; +export const ServerRequest__LegacyAppPathString = Schema.String; export type ServerRequest__McpElicitationArrayType = "array"; export const ServerRequest__McpElicitationArrayType = Schema.Literal("array"); @@ -3168,6 +3192,7 @@ export const V1InitializeParams__ClientInfo = Schema.Struct({ export type V1InitializeParams__InitializeCapabilities = { readonly experimentalApi?: boolean; + readonly mcpServerOpenaiFormElicitation?: boolean; readonly optOutNotificationMethods?: ReadonlyArray | null; readonly requestAttestation?: boolean; }; @@ -3178,6 +3203,11 @@ export const V1InitializeParams__InitializeCapabilities = Schema.Struct({ default: false, }), ), + mcpServerOpenaiFormElicitation: Schema.optionalKey( + Schema.Boolean.annotate({ + description: "Allow downstream MCP servers to request OpenAI extended form elicitations.", + }), + ), optOutNotificationMethods: Schema.optionalKey( Schema.Union([ Schema.Array(Schema.String).annotate({ @@ -3280,14 +3310,18 @@ export type V2AccountUpdatedNotification__AuthMode = | "apikey" | "chatgpt" | "chatgptAuthTokens" + | "headers" | "agentIdentity" - | "personalAccessToken"; + | "personalAccessToken" + | "bedrockApiKey"; export const V2AccountUpdatedNotification__AuthMode = Schema.Literals([ "apikey", "chatgpt", "chatgptAuthTokens", + "headers", "agentIdentity", "personalAccessToken", + "bedrockApiKey", ]).annotate({ description: "Authentication mode for OpenAI-backed providers." }); export type V2AccountUpdatedNotification__PlanType = @@ -3349,6 +3383,33 @@ export const V2AppListUpdatedNotification__AppScreenshot = Schema.Struct({ userPrompt: Schema.String, }); +export type V2AppsInstalledResponse__InstalledApp = { + readonly callable: boolean; + readonly enabled: boolean; + readonly id: string; + readonly runtimeName?: string | null; +}; +export const V2AppsInstalledResponse__InstalledApp = Schema.Struct({ + callable: Schema.Boolean.annotate({ + description: + "Whether the connector is enabled and has a non-synthetic, model-visible tool allowed by effective MCP and app/tool policy in the committed runtime snapshot.", + }), + enabled: Schema.Boolean.annotate({ + description: + "Effective enabled state after applying global, workspace, local, and managed configuration at read time.", + }), + id: Schema.String, + runtimeName: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Best-effort name carried by the runtime tool catalog. Canonical app metadata remains owned by `app/read`.", + }), + Schema.Null, + ]), + ), +}).annotate({ description: "Installed connector runtime state." }); + export type V2AppsListResponse__AppBranding = { readonly category?: string | null; readonly developer?: string | null; @@ -3380,6 +3441,17 @@ export const V2AppsListResponse__AppScreenshot = Schema.Struct({ userPrompt: Schema.String, }); +export type V2AppsReadResponse__AppToolSummary = { + readonly description: string; + readonly name: string; + readonly title?: string | null; +}; +export const V2AppsReadResponse__AppToolSummary = Schema.Struct({ + description: Schema.String, + name: Schema.String, + title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}).annotate({ description: "EXPERIMENTAL - metadata returned by app/read." }); + export type V2CancelLoginAccountResponse__CancelLoginAccountStatus = "canceled" | "notFound"; export const V2CancelLoginAccountResponse__CancelLoginAccountStatus = Schema.Literals([ "canceled", @@ -3429,8 +3501,13 @@ export const V2ConfigReadResponse__AnalyticsConfig = Schema.StructWithRest( [Schema.Record(Schema.String, Schema.Unknown)], ); -export type V2ConfigReadResponse__AppToolApproval = "auto" | "prompt" | "approve"; -export const V2ConfigReadResponse__AppToolApproval = Schema.Literals(["auto", "prompt", "approve"]); +export type V2ConfigReadResponse__AppToolApproval = "auto" | "prompt" | "writes" | "approve"; +export const V2ConfigReadResponse__AppToolApproval = Schema.Literals([ + "auto", + "prompt", + "writes", + "approve", +]); export type V2ConfigReadResponse__AppToolsConfig = {}; export const V2ConfigReadResponse__AppToolsConfig = Schema.Struct({}); @@ -3445,20 +3522,8 @@ export const V2ConfigReadResponse__ApprovalsReviewer = Schema.Literals([ "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", }); -export type V2ConfigReadResponse__AppsDefaultConfig = { - readonly destructive_enabled?: boolean; - readonly enabled?: boolean; - readonly open_world_enabled?: boolean; -}; -export const V2ConfigReadResponse__AppsDefaultConfig = Schema.Struct({ - destructive_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), - enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), - open_world_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), -}); - export type V2ConfigReadResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -3472,7 +3537,7 @@ export type V2ConfigReadResponse__AskForApproval = }; export const V2ConfigReadResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -3572,12 +3637,16 @@ export const V2ConfigReadResponse__WebSearchLocation = Schema.Struct({ timezone: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); -export type V2ConfigReadResponse__WebSearchMode = "disabled" | "cached" | "live"; -export const V2ConfigReadResponse__WebSearchMode = Schema.Literals(["disabled", "cached", "live"]); +export type V2ConfigReadResponse__WebSearchMode = "disabled" | "cached" | "indexed" | "live"; +export const V2ConfigReadResponse__WebSearchMode = Schema.Literals([ + "disabled", + "cached", + "indexed", + "live", +]); export type V2ConfigRequirementsReadResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -3591,7 +3660,7 @@ export type V2ConfigRequirementsReadResponse__AskForApproval = }; export const V2ConfigRequirementsReadResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -3662,6 +3731,11 @@ export const V2ConfigRequirementsReadResponse__NetworkUnixSocketPermission = Sch "deny", ]); +export type V2ConfigRequirementsReadResponse__ReasoningEffort = string; +export const V2ConfigRequirementsReadResponse__ReasoningEffort = Schema.String.annotate({ + description: "A non-empty reasoning effort value advertised by the model.", +}).check(Schema.isMinLength(1)); + export type V2ConfigRequirementsReadResponse__ResidencyRequirement = "us"; export const V2ConfigRequirementsReadResponse__ResidencyRequirement = Schema.Literal("us"); @@ -3675,10 +3749,15 @@ export const V2ConfigRequirementsReadResponse__SandboxMode = Schema.Literals([ "danger-full-access", ]); -export type V2ConfigRequirementsReadResponse__WebSearchMode = "disabled" | "cached" | "live"; +export type V2ConfigRequirementsReadResponse__WebSearchMode = + | "disabled" + | "cached" + | "indexed" + | "live"; export const V2ConfigRequirementsReadResponse__WebSearchMode = Schema.Literals([ "disabled", "cached", + "indexed", "live", ]); @@ -3716,6 +3795,11 @@ export const V2ConfigWriteResponse__AbsolutePathBuf = Schema.String.annotate({ export type V2ConfigWriteResponse__WriteStatus = "ok" | "okOverridden"; export const V2ConfigWriteResponse__WriteStatus = Schema.Literals(["ok", "okOverridden"]); +export type V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = + "reset" | "nothingToReset" | "noCredit" | "alreadyRedeemed"; +export const V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome = + Schema.Literals(["reset", "nothingToReset", "noCredit", "alreadyRedeemed"]); + export type V2ErrorNotification__NonSteerableTurnKind = "review" | "compact"; export const V2ErrorNotification__NonSteerableTurnKind = Schema.Literals(["review", "compact"]); @@ -3784,6 +3868,7 @@ export type V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationIte | "SUBAGENTS" | "HOOKS" | "COMMANDS" + | "MEMORY" | "SESSIONS"; export const V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationItemType = Schema.Literals([ @@ -3795,6 +3880,7 @@ export const V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationIt "SUBAGENTS", "HOOKS", "COMMANDS", + "MEMORY", "SESSIONS", ]); @@ -3828,11 +3914,71 @@ export const V2ExternalAgentConfigDetectResponse__SessionMigration = Schema.Stru title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2ExternalAgentConfigDetectResponse__SkillMigration = { readonly name: string }; +export const V2ExternalAgentConfigDetectResponse__SkillMigration = Schema.Struct({ + name: Schema.String, +}); + export type V2ExternalAgentConfigDetectResponse__SubagentMigration = { readonly name: string }; export const V2ExternalAgentConfigDetectResponse__SubagentMigration = Schema.Struct({ name: Schema.String, }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS", + ]); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS", + ]); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorSource = + "remoteMcpServersConfig"; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorSource = + Schema.Literal("remoteMcpServersConfig"); + export type V2ExternalAgentConfigImportParams__CommandMigration = { readonly name: string }; export const V2ExternalAgentConfigImportParams__CommandMigration = Schema.Struct({ name: Schema.String, @@ -3847,6 +3993,7 @@ export type V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItemT | "SUBAGENTS" | "HOOKS" | "COMMANDS" + | "MEMORY" | "SESSIONS"; export const V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItemType = Schema.Literals([ @@ -3858,6 +4005,7 @@ export const V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem "SUBAGENTS", "HOOKS", "COMMANDS", + "MEMORY", "SESSIONS", ]); @@ -3891,11 +4039,41 @@ export const V2ExternalAgentConfigImportParams__SessionMigration = Schema.Struct title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2ExternalAgentConfigImportParams__SkillMigration = { readonly name: string }; +export const V2ExternalAgentConfigImportParams__SkillMigration = Schema.Struct({ + name: Schema.String, +}); + export type V2ExternalAgentConfigImportParams__SubagentMigration = { readonly name: string }; export const V2ExternalAgentConfigImportParams__SubagentMigration = Schema.Struct({ name: Schema.String, }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "MEMORY" + | "SESSIONS"; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType = + Schema.Literals([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS", + ]); + export type V2FileChangePatchUpdatedNotification__PatchChangeKind = | { readonly type: "add" } | { readonly type: "delete" } @@ -3992,6 +4170,24 @@ export const V2GetAccountRateLimitsResponse__RateLimitReachedType = Schema.Liter "workspace_member_usage_limit_reached", ]); +export type V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus = + | "available" + | "redeeming" + | "redeemed" + | "unknown"; +export const V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus = Schema.Literals([ + "available", + "redeeming", + "redeemed", + "unknown", +]); + +export type V2GetAccountRateLimitsResponse__RateLimitResetType = "codexRateLimits" | "unknown"; +export const V2GetAccountRateLimitsResponse__RateLimitResetType = Schema.Literals([ + "codexRateLimits", + "unknown", +]); + export type V2GetAccountRateLimitsResponse__RateLimitWindow = { readonly resetsAt?: number | null; readonly usedPercent: number; @@ -4082,6 +4278,16 @@ export const V2GetAccountTokenUsageResponse__AccountTokenUsageSummary = Schema.S ), }); +export type V2GetWorkspaceMessagesResponse__WorkspaceMessageType = + | "headline" + | "announcement" + | "unknown"; +export const V2GetWorkspaceMessagesResponse__WorkspaceMessageType = Schema.Literals([ + "headline", + "announcement", + "unknown", +]); + export type V2HookCompletedNotification__AbsolutePathBuf = string; export const V2HookCompletedNotification__AbsolutePathBuf = Schema.String.annotate({ description: @@ -4095,6 +4301,7 @@ export type V2HookCompletedNotification__HookEventName = | "preCompact" | "postCompact" | "sessionStart" + | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" @@ -4106,6 +4313,7 @@ export const V2HookCompletedNotification__HookEventName = Schema.Literals([ "preCompact", "postCompact", "sessionStart", + "sessionEnd", "userPromptSubmit", "subagentStart", "subagentStop", @@ -4175,6 +4383,7 @@ export type V2HooksListResponse__HookEventName = | "preCompact" | "postCompact" | "sessionStart" + | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" @@ -4186,6 +4395,7 @@ export const V2HooksListResponse__HookEventName = Schema.Literals([ "preCompact", "postCompact", "sessionStart", + "sessionEnd", "userPromptSubmit", "subagentStart", "subagentStop", @@ -4242,6 +4452,7 @@ export type V2HookStartedNotification__HookEventName = | "preCompact" | "postCompact" | "sessionStart" + | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" @@ -4253,6 +4464,7 @@ export const V2HookStartedNotification__HookEventName = Schema.Literals([ "preCompact", "postCompact", "sessionStart", + "sessionEnd", "userPromptSubmit", "subagentStart", "subagentStop", @@ -4338,7 +4550,8 @@ export const V2ItemCompletedNotification__CommandExecutionStatus = Schema.Litera export type V2ItemCompletedNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ItemCompletedNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -4353,6 +4566,12 @@ export const V2ItemCompletedNotification__DynamicToolCallOutputContentItem = Sch title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -4384,6 +4603,24 @@ export const V2ItemCompletedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ItemCompletedNotification__LegacyAppPathString = string; +export const V2ItemCompletedNotification__LegacyAppPathString = Schema.String; + +export type V2ItemCompletedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ItemCompletedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ItemCompletedNotification__McpToolCallError = { readonly message: string }; export const V2ItemCompletedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -4563,41 +4800,6 @@ export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessM export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessMode = Schema.Literals(["read", "write", "deny"]); -export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath = - Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, - ); - export type V2ItemGuardianApprovalReviewCompletedNotification__GuardianApprovalReviewStatus = | "inProgress" | "approved" @@ -4634,6 +4836,9 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__GuardianUserAuth description: "[UNSTABLE] Authorization level assigned by approval auto-review.", }); +export type V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString = string; +export const V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString = Schema.String; + export type V2ItemGuardianApprovalReviewCompletedNotification__NetworkApprovalProtocol = | "http" | "https" @@ -4662,40 +4867,6 @@ export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMod export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMode = Schema.Literals(["read", "write", "deny"]); -export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath = - | { readonly kind: "root" } - | { readonly kind: "minimal" } - | { readonly kind: "project_roots"; readonly subpath?: string | null } - | { readonly kind: "tmpdir" } - | { readonly kind: "slash_tmp" } - | { readonly kind: "unknown"; readonly path: string; readonly subpath?: string | null }; -export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath = Schema.Union( - [ - Schema.Struct({ kind: Schema.Literal("root") }).annotate({ - title: "RootFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ - title: "MinimalFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("project_roots"), - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }).annotate({ title: "KindFileSystemSpecialPath" }), - Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ - title: "TmpdirFileSystemSpecialPath", - }), - Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ - title: "SlashTmpFileSystemSpecialPath", - }), - Schema.Struct({ - kind: Schema.Literal("unknown"), - path: Schema.String, - subpath: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - }), - ], - { mode: "oneOf" }, -); - export type V2ItemGuardianApprovalReviewStartedNotification__GuardianApprovalReviewStatus = | "inProgress" | "approved" @@ -4735,6 +4906,9 @@ export const V2ItemGuardianApprovalReviewStartedNotification__GuardianUserAuthor description: "[UNSTABLE] Authorization level assigned by approval auto-review.", }); +export type V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString = string; +export const V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString = Schema.String; + export type V2ItemGuardianApprovalReviewStartedNotification__NetworkApprovalProtocol = | "http" | "https" @@ -4781,7 +4955,8 @@ export const V2ItemStartedNotification__CommandExecutionStatus = Schema.Literals export type V2ItemStartedNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ItemStartedNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -4796,6 +4971,12 @@ export const V2ItemStartedNotification__DynamicToolCallOutputContentItem = Schem title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -4827,6 +5008,24 @@ export const V2ItemStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ItemStartedNotification__LegacyAppPathString = string; +export const V2ItemStartedNotification__LegacyAppPathString = Schema.String; + +export type V2ItemStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ItemStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ItemStartedNotification__McpToolCallError = { readonly message: string }; export const V2ItemStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -5078,6 +5277,9 @@ export const V2ListMcpServerStatusResponse__Tool = Schema.Struct({ title: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }).annotate({ description: "Definition for a tool the client can call." }); +export type V2LoginAccountParams__LoginAppBrand = "codex" | "chatgpt"; +export const V2LoginAccountParams__LoginAppBrand = Schema.Literals(["codex", "chatgpt"]); + export type V2MarketplaceAddResponse__AbsolutePathBuf = string; export const V2MarketplaceAddResponse__AbsolutePathBuf = Schema.String.annotate({ description: @@ -5133,6 +5335,12 @@ export const V2McpResourceReadResponse__ResourceContent = Schema.Union([ }), ]).annotate({ description: "Contents returned when reading a resource from an MCP server." }); +export type V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason = + "reauthenticationRequired"; +export const V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason = Schema.Literal( + "reauthenticationRequired", +); + export type V2McpServerStatusUpdatedNotification__McpServerStartupState = | "starting" | "ready" @@ -5145,10 +5353,12 @@ export const V2McpServerStatusUpdatedNotification__McpServerStartupState = Schem "cancelled", ]); -export type V2ModelListResponse__InputModality = "text" | "image"; -export const V2ModelListResponse__InputModality = Schema.Literals(["text", "image"]).annotate({ - description: "Canonical user-input modality tags advertised by a model.", -}); +export type V2ModelListResponse__InputModality = "text" | "image" | "audio"; +export const V2ModelListResponse__InputModality = Schema.Literals([ + "text", + "image", + "audio", +]).annotate({ description: "Canonical user-input modality tags advertised by a model." }); export type V2ModelListResponse__ModelAvailabilityNux = { readonly message: string }; export const V2ModelListResponse__ModelAvailabilityNux = Schema.Struct({ message: Schema.String }); @@ -5191,10 +5401,14 @@ export const V2ModelVerificationNotification__ModelVerification = Schema.Literal("trustedAccessForCyber"); export type V2PermissionProfileListResponse__PermissionProfileSummary = { + readonly allowed: boolean; readonly description?: string | null; readonly id: string; }; export const V2PermissionProfileListResponse__PermissionProfileSummary = Schema.Struct({ + allowed: Schema.Boolean.annotate({ + description: "Whether the effective requirements allow selecting this profile.", + }), description: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -5241,6 +5455,14 @@ export const V2PluginInstalledResponse__PluginInstallPolicy = Schema.Literals([ "INSTALLED_BY_DEFAULT", ]); +export type V2PluginInstalledResponse__PluginInstallPolicySource = + | "WORKSPACE_SETTING" + | "IMPLICIT_CANONICAL_APP"; +export const V2PluginInstalledResponse__PluginInstallPolicySource = Schema.Literals([ + "WORKSPACE_SETTING", + "IMPLICIT_CANONICAL_APP", +]); + export type V2PluginInstalledResponse__PluginShareDiscoverability = | "LISTED" | "UNLISTED" @@ -5272,18 +5494,18 @@ export const V2PluginInstallParams__AbsolutePathBuf = Schema.String.annotate({ }); export type V2PluginInstallResponse__AppSummary = { + readonly category?: string | null; readonly description?: string | null; readonly id: string; readonly installUrl?: string | null; readonly name: string; - readonly needsAuth: boolean; }; export const V2PluginInstallResponse__AppSummary = Schema.Struct({ + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, - needsAuth: Schema.Boolean, }).annotate({ description: "EXPERIMENTAL - app metadata summary for plugin responses." }); export type V2PluginInstallResponse__PluginAuthPolicy = "ON_INSTALL" | "ON_USE"; @@ -5299,12 +5521,14 @@ export type V2PluginListParams__PluginListMarketplaceKind = | "local" | "vertical" | "workspace-directory" - | "shared-with-me"; + | "shared-with-me" + | "created-by-me-remote"; export const V2PluginListParams__PluginListMarketplaceKind = Schema.Literals([ "local", "vertical", "workspace-directory", "shared-with-me", + "created-by-me-remote", ]); export type V2PluginListResponse__AbsolutePathBuf = string; @@ -5331,6 +5555,14 @@ export const V2PluginListResponse__PluginInstallPolicy = Schema.Literals([ "INSTALLED_BY_DEFAULT", ]); +export type V2PluginListResponse__PluginInstallPolicySource = + | "WORKSPACE_SETTING" + | "IMPLICIT_CANONICAL_APP"; +export const V2PluginListResponse__PluginInstallPolicySource = Schema.Literals([ + "WORKSPACE_SETTING", + "IMPLICIT_CANONICAL_APP", +]); + export type V2PluginListResponse__PluginShareDiscoverability = "LISTED" | "UNLISTED" | "PRIVATE"; export const V2PluginListResponse__PluginShareDiscoverability = Schema.Literals([ "LISTED", @@ -5365,18 +5597,18 @@ export const V2PluginReadResponse__AbsolutePathBuf = Schema.String.annotate({ }); export type V2PluginReadResponse__AppSummary = { + readonly category?: string | null; readonly description?: string | null; readonly id: string; readonly installUrl?: string | null; readonly name: string; - readonly needsAuth: boolean; }; export const V2PluginReadResponse__AppSummary = Schema.Struct({ + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, - needsAuth: Schema.Boolean, }).annotate({ description: "EXPERIMENTAL - app metadata summary for plugin responses." }); export type V2PluginReadResponse__AppTemplateUnavailableReason = @@ -5394,6 +5626,7 @@ export type V2PluginReadResponse__HookEventName = | "preCompact" | "postCompact" | "sessionStart" + | "sessionEnd" | "userPromptSubmit" | "subagentStart" | "subagentStop" @@ -5405,6 +5638,7 @@ export const V2PluginReadResponse__HookEventName = Schema.Literals([ "preCompact", "postCompact", "sessionStart", + "sessionEnd", "userPromptSubmit", "subagentStart", "subagentStop", @@ -5424,6 +5658,14 @@ export const V2PluginReadResponse__PluginInstallPolicy = Schema.Literals([ "INSTALLED_BY_DEFAULT", ]); +export type V2PluginReadResponse__PluginInstallPolicySource = + | "WORKSPACE_SETTING" + | "IMPLICIT_CANONICAL_APP"; +export const V2PluginReadResponse__PluginInstallPolicySource = Schema.Literals([ + "WORKSPACE_SETTING", + "IMPLICIT_CANONICAL_APP", +]); + export type V2PluginReadResponse__PluginShareDiscoverability = "LISTED" | "UNLISTED" | "PRIVATE"; export const V2PluginReadResponse__PluginShareDiscoverability = Schema.Literals([ "LISTED", @@ -5445,6 +5687,24 @@ export const V2PluginReadResponse__PluginSharePrincipalType = Schema.Literals([ "workspace", ]); +export type V2PluginReadResponse__ScheduledTaskWeekday = + | "MO" + | "TU" + | "WE" + | "TH" + | "FR" + | "SA" + | "SU"; +export const V2PluginReadResponse__ScheduledTaskWeekday = Schema.Literals([ + "MO", + "TU", + "WE", + "TH", + "FR", + "SA", + "SU", +]); + export type V2PluginShareCheckoutResponse__AbsolutePathBuf = string; export const V2PluginShareCheckoutResponse__AbsolutePathBuf = Schema.String.annotate({ description: @@ -5473,6 +5733,14 @@ export const V2PluginShareListResponse__PluginInstallPolicy = Schema.Literals([ "INSTALLED_BY_DEFAULT", ]); +export type V2PluginShareListResponse__PluginInstallPolicySource = + | "WORKSPACE_SETTING" + | "IMPLICIT_CANONICAL_APP"; +export const V2PluginShareListResponse__PluginInstallPolicySource = Schema.Literals([ + "WORKSPACE_SETTING", + "IMPLICIT_CANONICAL_APP", +]); + export type V2PluginShareListResponse__PluginShareDiscoverability = | "LISTED" | "UNLISTED" @@ -5538,10 +5806,12 @@ export const V2PluginShareUpdateTargetsParams__PluginShareTargetRole = Schema.Li export type V2PluginShareUpdateTargetsParams__PluginShareUpdateDiscoverability = | "UNLISTED" - | "PRIVATE"; + | "PRIVATE" + | "LISTED"; export const V2PluginShareUpdateTargetsParams__PluginShareUpdateDiscoverability = Schema.Literals([ "UNLISTED", "PRIVATE", + "LISTED", ]); export type V2PluginShareUpdateTargetsResponse__PluginShareDiscoverability = @@ -5574,12 +5844,36 @@ export const V2PluginShareUpdateTargetsResponse__PluginSharePrincipalType = Sche "workspace", ]); -export type V2RawResponseItemCompletedNotification__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; +export type V2RawResponseCompletedNotification__TokenUsageBreakdown = { + readonly cacheWriteInputTokens?: number; + readonly cachedInputTokens: number; + readonly inputTokens: number; + readonly outputTokens: number; + readonly reasoningOutputTokens: number; + readonly totalTokens: number; }; +export const V2RawResponseCompletedNotification__TokenUsageBreakdown = Schema.Struct({ + cacheWriteInputTokens: Schema.optionalKey( + Schema.Number.annotate({ default: 0, format: "int64" }).check(Schema.isInt()), + ), + cachedInputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + inputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + outputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + reasoningOutputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + totalTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), +}); + +export type V2RawResponseItemCompletedNotification__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2RawResponseItemCompletedNotification__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -5602,6 +5896,17 @@ export const V2RawResponseItemCompletedNotification__ImageDetail = Schema.Litera "original", ]); +export type V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough = + Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", + }); + export type V2RawResponseItemCompletedNotification__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -5824,7 +6129,8 @@ export const V2ReviewStartResponse__CommandExecutionStatus = Schema.Literals([ export type V2ReviewStartResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ReviewStartResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -5839,6 +6145,12 @@ export const V2ReviewStartResponse__DynamicToolCallOutputContentItem = Schema.Un title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -5867,6 +6179,24 @@ export const V2ReviewStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ReviewStartResponse__LegacyAppPathString = string; +export const V2ReviewStartResponse__LegacyAppPathString = Schema.String; + +export type V2ReviewStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ReviewStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ReviewStartResponse__McpToolCallError = { readonly message: string }; export const V2ReviewStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6114,7 +6444,6 @@ export const V2ThreadForkParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadForkParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -6128,7 +6457,7 @@ export type V2ThreadForkParams__AskForApproval = }; export const V2ThreadForkParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -6166,7 +6495,6 @@ export const V2ThreadForkResponse__AgentPath = Schema.String; export type V2ThreadForkResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -6180,7 +6508,7 @@ export type V2ThreadForkResponse__AskForApproval = }; export const V2ThreadForkResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -6226,7 +6554,8 @@ export const V2ThreadForkResponse__CommandExecutionStatus = Schema.Literals([ export type V2ThreadForkResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadForkResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -6241,6 +6570,12 @@ export const V2ThreadForkResponse__DynamicToolCallOutputContentItem = Schema.Uni title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -6280,6 +6615,24 @@ export const V2ThreadForkResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadForkResponse__LegacyAppPathString = string; +export const V2ThreadForkResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadForkResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadForkResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadForkResponse__McpToolCallError = { readonly message: string }; export const V2ThreadForkResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6528,8 +6881,12 @@ export const V2ThreadListParams__ThreadListCwdFilter = Schema.Union([ Schema.Array(Schema.String), ]); -export type V2ThreadListParams__ThreadSortKey = "created_at" | "updated_at"; -export const V2ThreadListParams__ThreadSortKey = Schema.Literals(["created_at", "updated_at"]); +export type V2ThreadListParams__ThreadSortKey = "created_at" | "updated_at" | "recency_at"; +export const V2ThreadListParams__ThreadSortKey = Schema.Literals([ + "created_at", + "updated_at", + "recency_at", +]); export type V2ThreadListParams__ThreadSourceKind = | "cli" @@ -6596,7 +6953,8 @@ export const V2ThreadListResponse__CommandExecutionStatus = Schema.Literals([ export type V2ThreadListResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadListResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -6611,6 +6969,12 @@ export const V2ThreadListResponse__DynamicToolCallOutputContentItem = Schema.Uni title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -6650,6 +7014,24 @@ export const V2ThreadListResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadListResponse__LegacyAppPathString = string; +export const V2ThreadListResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadListResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadListResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadListResponse__McpToolCallError = { readonly message: string }; export const V2ThreadListResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -6901,7 +7283,8 @@ export const V2ThreadMetadataUpdateResponse__CommandExecutionStatus = Schema.Lit export type V2ThreadMetadataUpdateResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadMetadataUpdateResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -6916,6 +7299,12 @@ export const V2ThreadMetadataUpdateResponse__DynamicToolCallOutputContentItem = title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -6958,6 +7347,24 @@ export const V2ThreadMetadataUpdateResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadMetadataUpdateResponse__LegacyAppPathString = string; +export const V2ThreadMetadataUpdateResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadMetadataUpdateResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadMetadataUpdateResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadMetadataUpdateResponse__McpToolCallError = { readonly message: string }; export const V2ThreadMetadataUpdateResponse__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -7187,7 +7594,8 @@ export const V2ThreadReadResponse__CommandExecutionStatus = Schema.Literals([ export type V2ThreadReadResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadReadResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -7202,6 +7610,12 @@ export const V2ThreadReadResponse__DynamicToolCallOutputContentItem = Schema.Uni title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -7241,6 +7655,24 @@ export const V2ThreadReadResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadReadResponse__LegacyAppPathString = string; +export const V2ThreadReadResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadReadResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadReadResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadReadResponse__McpToolCallError = { readonly message: string }; export const V2ThreadReadResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -7444,18 +7876,24 @@ export const V2ThreadRealtimeOutputAudioDeltaNotification__ThreadRealtimeAudioCh }, ).annotate({ description: "EXPERIMENTAL - thread realtime audio chunk." }); -export type V2ThreadRealtimeStartedNotification__RealtimeConversationVersion = "v1" | "v2"; +export type V2ThreadRealtimeStartedNotification__RealtimeConversationVersion = "v1" | "v2" | "v3"; export const V2ThreadRealtimeStartedNotification__RealtimeConversationVersion = Schema.Literals([ "v1", "v2", + "v3", ]); -export type V2ThreadResumeParams__AgentMessageInputContent = { - readonly encrypted_content: string; - readonly type: "encrypted_content"; -}; +export type V2ThreadResumeParams__AgentMessageInputContent = + | { readonly text: string; readonly type: "input_text" } + | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2ThreadResumeParams__AgentMessageInputContent = Schema.Union( [ + Schema.Struct({ + text: Schema.String, + type: Schema.Literal("input_text").annotate({ + title: "InputTextAgentMessageInputContentType", + }), + }).annotate({ title: "InputTextAgentMessageInputContent" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -7478,7 +7916,6 @@ export const V2ThreadResumeParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadResumeParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -7492,7 +7929,7 @@ export type V2ThreadResumeParams__AskForApproval = }; export const V2ThreadResumeParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -7514,6 +7951,16 @@ export const V2ThreadResumeParams__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadResumeParams__InternalChatMessageMetadataPassthrough = { + readonly turn_id?: string | null; +}; +export const V2ThreadResumeParams__InternalChatMessageMetadataPassthrough = Schema.Struct({ + turn_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}).annotate({ + description: + "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", +}); + export type V2ThreadResumeParams__LocalShellAction = { readonly command: ReadonlyArray; readonly env?: { readonly [x: string]: string } | null; @@ -7670,7 +8117,6 @@ export const V2ThreadResumeResponse__AgentPath = Schema.String; export type V2ThreadResumeResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -7684,7 +8130,7 @@ export type V2ThreadResumeResponse__AskForApproval = }; export const V2ThreadResumeResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -7730,7 +8176,8 @@ export const V2ThreadResumeResponse__CommandExecutionStatus = Schema.Literals([ export type V2ThreadResumeResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadResumeResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -7745,6 +8192,12 @@ export const V2ThreadResumeResponse__DynamicToolCallOutputContentItem = Schema.U title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -7784,6 +8237,24 @@ export const V2ThreadResumeResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadResumeResponse__LegacyAppPathString = string; +export const V2ThreadResumeResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadResumeResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadResumeResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadResumeResponse__McpToolCallError = { readonly message: string }; export const V2ThreadResumeResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -8000,7 +8471,8 @@ export const V2ThreadRollbackResponse__CommandExecutionStatus = Schema.Literals( export type V2ThreadRollbackResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadRollbackResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -8015,6 +8487,12 @@ export const V2ThreadRollbackResponse__DynamicToolCallOutputContentItem = Schema title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -8054,6 +8532,24 @@ export const V2ThreadRollbackResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadRollbackResponse__LegacyAppPathString = string; +export const V2ThreadRollbackResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadRollbackResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadRollbackResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadRollbackResponse__McpToolCallError = { readonly message: string }; export const V2ThreadRollbackResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -8276,7 +8772,6 @@ export const V2ThreadSettingsUpdatedNotification__ApprovalsReviewer = Schema.Lit export type V2ThreadSettingsUpdatedNotification__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8290,7 +8785,7 @@ export type V2ThreadSettingsUpdatedNotification__AskForApproval = }; export const V2ThreadSettingsUpdatedNotification__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8379,7 +8874,8 @@ export const V2ThreadStartedNotification__CommandExecutionStatus = Schema.Litera export type V2ThreadStartedNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadStartedNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -8394,6 +8890,12 @@ export const V2ThreadStartedNotification__DynamicToolCallOutputContentItem = Sch title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -8436,6 +8938,24 @@ export const V2ThreadStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadStartedNotification__LegacyAppPathString = string; +export const V2ThreadStartedNotification__LegacyAppPathString = Schema.String; + +export type V2ThreadStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadStartedNotification__McpToolCallError = { readonly message: string }; export const V2ThreadStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -8621,12 +9141,6 @@ export const V2ThreadStartedNotification__WebSearchAction = Schema.Union( { mode: "oneOf" }, ); -export type V2ThreadStartParams__AbsolutePathBuf = string; -export const V2ThreadStartParams__AbsolutePathBuf = Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", -}); - export type V2ThreadStartParams__ApprovalsReviewer = "user" | "auto_review" | "guardian_subagent"; export const V2ThreadStartParams__ApprovalsReviewer = Schema.Literals([ "user", @@ -8639,7 +9153,6 @@ export const V2ThreadStartParams__ApprovalsReviewer = Schema.Literals([ export type V2ThreadStartParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8653,7 +9166,7 @@ export type V2ThreadStartParams__AskForApproval = }; export const V2ThreadStartParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8667,6 +9180,29 @@ export const V2ThreadStartParams__AskForApproval = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartParams__DynamicToolNamespaceTool = { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; +}; +export const V2ThreadStartParams__DynamicToolNamespaceTool = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolNamespaceToolType" }), + }).annotate({ title: "FunctionDynamicToolNamespaceTool" }), + ], + { mode: "oneOf" }, +); + +export type V2ThreadStartParams__LegacyAppPathString = string; +export const V2ThreadStartParams__LegacyAppPathString = Schema.String; + export type V2ThreadStartParams__Personality = "none" | "friendly" | "pragmatic"; export const V2ThreadStartParams__Personality = Schema.Literals(["none", "friendly", "pragmatic"]); @@ -8697,7 +9233,6 @@ export const V2ThreadStartResponse__AgentPath = Schema.String; export type V2ThreadStartResponse__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -8711,7 +9246,7 @@ export type V2ThreadStartResponse__AskForApproval = }; export const V2ThreadStartResponse__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -8757,7 +9292,8 @@ export const V2ThreadStartResponse__CommandExecutionStatus = Schema.Literals([ export type V2ThreadStartResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadStartResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -8772,6 +9308,12 @@ export const V2ThreadStartResponse__DynamicToolCallOutputContentItem = Schema.Un title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -8811,6 +9353,24 @@ export const V2ThreadStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadStartResponse__LegacyAppPathString = string; +export const V2ThreadStartResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadStartResponse__McpToolCallError = { readonly message: string }; export const V2ThreadStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -8995,6 +9555,7 @@ export const V2ThreadStatusChangedNotification__ThreadActiveFlag = Schema.Litera ]); export type V2ThreadTokenUsageUpdatedNotification__TokenUsageBreakdown = { + readonly cacheWriteInputTokens?: number; readonly cachedInputTokens: number; readonly inputTokens: number; readonly outputTokens: number; @@ -9002,6 +9563,9 @@ export type V2ThreadTokenUsageUpdatedNotification__TokenUsageBreakdown = { readonly totalTokens: number; }; export const V2ThreadTokenUsageUpdatedNotification__TokenUsageBreakdown = Schema.Struct({ + cacheWriteInputTokens: Schema.optionalKey( + Schema.Number.annotate({ default: 0, format: "int64" }).check(Schema.isInt()), + ), cachedInputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), inputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), outputTokens: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), @@ -9050,7 +9614,8 @@ export const V2ThreadUnarchiveResponse__CommandExecutionStatus = Schema.Literals export type V2ThreadUnarchiveResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2ThreadUnarchiveResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -9065,6 +9630,12 @@ export const V2ThreadUnarchiveResponse__DynamicToolCallOutputContentItem = Schem title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -9107,6 +9678,24 @@ export const V2ThreadUnarchiveResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2ThreadUnarchiveResponse__LegacyAppPathString = string; +export const V2ThreadUnarchiveResponse__LegacyAppPathString = Schema.String; + +export type V2ThreadUnarchiveResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2ThreadUnarchiveResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2ThreadUnarchiveResponse__McpToolCallError = { readonly message: string }; export const V2ThreadUnarchiveResponse__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9340,7 +9929,8 @@ export const V2TurnCompletedNotification__CommandExecutionStatus = Schema.Litera export type V2TurnCompletedNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2TurnCompletedNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -9355,6 +9945,12 @@ export const V2TurnCompletedNotification__DynamicToolCallOutputContentItem = Sch title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -9386,6 +9982,24 @@ export const V2TurnCompletedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnCompletedNotification__LegacyAppPathString = string; +export const V2TurnCompletedNotification__LegacyAppPathString = Schema.String; + +export type V2TurnCompletedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2TurnCompletedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnCompletedNotification__McpToolCallError = { readonly message: string }; export const V2TurnCompletedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9605,7 +10219,8 @@ export const V2TurnStartedNotification__CommandExecutionStatus = Schema.Literals export type V2TurnStartedNotification__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2TurnStartedNotification__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -9620,6 +10235,12 @@ export const V2TurnStartedNotification__DynamicToolCallOutputContentItem = Schem title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -9651,6 +10272,24 @@ export const V2TurnStartedNotification__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnStartedNotification__LegacyAppPathString = string; +export const V2TurnStartedNotification__LegacyAppPathString = Schema.String; + +export type V2TurnStartedNotification__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2TurnStartedNotification__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnStartedNotification__McpToolCallError = { readonly message: string }; export const V2TurnStartedNotification__McpToolCallError = Schema.Struct({ message: Schema.String, @@ -9846,7 +10485,6 @@ export const V2TurnStartParams__ApprovalsReviewer = Schema.Literals([ export type V2TurnStartParams__AskForApproval = | "untrusted" - | "on-failure" | "on-request" | "never" | { @@ -9860,7 +10498,7 @@ export type V2TurnStartParams__AskForApproval = }; export const V2TurnStartParams__AskForApproval = Schema.Union( [ - Schema.Literals(["untrusted", "on-failure", "on-request", "never"]), + Schema.Literals(["untrusted", "on-request", "never"]), Schema.Struct({ granular: Schema.Struct({ mcp_elicitations: Schema.Boolean, @@ -9877,6 +10515,9 @@ export const V2TurnStartParams__AskForApproval = Schema.Union( export type V2TurnStartParams__ImageDetail = "auto" | "low" | "high" | "original"; export const V2TurnStartParams__ImageDetail = Schema.Literals(["auto", "low", "high", "original"]); +export type V2TurnStartParams__LegacyAppPathString = string; +export const V2TurnStartParams__LegacyAppPathString = Schema.String; + export type V2TurnStartParams__ModeKind = "plan" | "default"; export const V2TurnStartParams__ModeKind = Schema.Literals(["plan", "default"]).annotate({ description: "Initial collaboration mode to use when the TUI starts.", @@ -9965,7 +10606,8 @@ export const V2TurnStartResponse__CommandExecutionStatus = Schema.Literals([ export type V2TurnStartResponse__DynamicToolCallOutputContentItem = | { readonly text: string; readonly type: "inputText" } - | { readonly imageUrl: string; readonly type: "inputImage" }; + | { readonly imageUrl: string; readonly type: "inputImage" } + | { readonly audioUrl: string; readonly type: "inputAudio" }; export const V2TurnStartResponse__DynamicToolCallOutputContentItem = Schema.Union( [ Schema.Struct({ @@ -9980,6 +10622,12 @@ export const V2TurnStartResponse__DynamicToolCallOutputContentItem = Schema.Unio title: "InputImageDynamicToolCallOutputContentItemType", }), }).annotate({ title: "InputImageDynamicToolCallOutputContentItem" }), + Schema.Struct({ + audioUrl: Schema.String, + type: Schema.Literal("inputAudio").annotate({ + title: "InputAudioDynamicToolCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioDynamicToolCallOutputContentItem" }), ], { mode: "oneOf" }, ); @@ -10008,6 +10656,24 @@ export const V2TurnStartResponse__ImageDetail = Schema.Literals([ "original", ]); +export type V2TurnStartResponse__LegacyAppPathString = string; +export const V2TurnStartResponse__LegacyAppPathString = Schema.String; + +export type V2TurnStartResponse__McpToolCallAppContext = { + readonly actionName?: string | null; + readonly appName?: string | null; + readonly connectorId: string; + readonly linkId?: string | null; + readonly resourceUri?: string | null; +}; +export const V2TurnStartResponse__McpToolCallAppContext = Schema.Struct({ + actionName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + appName: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + connectorId: Schema.String, + linkId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + resourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + export type V2TurnStartResponse__McpToolCallError = { readonly message: string }; export const V2TurnStartResponse__McpToolCallError = Schema.Struct({ message: Schema.String }); @@ -10367,6 +11033,7 @@ export type ClientRequest__ContentItem = readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly text: string; readonly type: "output_text" }; export const ClientRequest__ContentItem = Schema.Union( [ @@ -10379,6 +11046,10 @@ export const ClientRequest__ContentItem = Schema.Union( image_url: Schema.String, type: Schema.Literal("input_image").annotate({ title: "InputImageContentItemType" }), }).annotate({ title: "InputImageContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ title: "InputAudioContentItemType" }), + }).annotate({ title: "InputAudioContentItem" }), Schema.Struct({ text: Schema.String, type: Schema.Literal("output_text").annotate({ title: "OutputTextContentItemType" }), @@ -10394,6 +11065,7 @@ export type ClientRequest__FunctionCallOutputContentItem = readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const ClientRequest__FunctionCallOutputContentItem = Schema.Union( [ @@ -10410,6 +11082,12 @@ export const ClientRequest__FunctionCallOutputContentItem = Schema.Union( title: "InputImageFunctionCallOutputContentItemType", }), }).annotate({ title: "InputImageFunctionCallOutputContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ + title: "InputAudioFunctionCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioFunctionCallOutputContentItem" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -10434,6 +11112,78 @@ export const ClientRequest__InitializeParams = Schema.Struct({ clientInfo: ClientRequest__ClientInfo, }); +export type ClientRequest__LoginAccountParams = + | { readonly apiKey: string; readonly type: "apiKey" } + | { + readonly appBrand?: ClientRequest__LoginAppBrand | null; + readonly codexStreamlinedLogin?: boolean; + readonly type: "chatgpt"; + readonly useHostedLoginSuccessPage?: boolean; + } + | { readonly type: "chatgptDeviceCode" } + | { + readonly accessToken: string; + readonly chatgptAccountId: string; + readonly chatgptPlanType?: string | null; + readonly type: "chatgptAuthTokens"; + } + | { readonly apiKey: string; readonly region: string; readonly type: "amazonBedrock" }; +export const ClientRequest__LoginAccountParams = Schema.Union( + [ + Schema.Struct({ + apiKey: Schema.String, + type: Schema.Literal("apiKey").annotate({ title: "ApiKeyLoginAccountParamsType" }), + }).annotate({ title: "ApiKeyLoginAccountParams" }), + Schema.Struct({ + appBrand: Schema.optionalKey(Schema.Union([ClientRequest__LoginAppBrand, Schema.Null])), + codexStreamlinedLogin: Schema.optionalKey(Schema.Boolean), + type: Schema.Literal("chatgpt").annotate({ title: "ChatgptLoginAccountParamsType" }), + useHostedLoginSuccessPage: Schema.optionalKey(Schema.Boolean), + }).annotate({ title: "ChatgptLoginAccountParams" }), + Schema.Struct({ + type: Schema.Literal("chatgptDeviceCode").annotate({ + title: "ChatgptDeviceCodeLoginAccountParamsType", + }), + }).annotate({ title: "ChatgptDeviceCodeLoginAccountParams" }), + Schema.Struct({ + accessToken: Schema.String.annotate({ + description: + "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", + }), + chatgptAccountId: Schema.String.annotate({ + description: "Workspace/account identifier supplied by the client.", + }), + chatgptPlanType: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("chatgptAuthTokens").annotate({ + title: "ChatgptAuthTokensLoginAccountParamsType", + }), + }).annotate({ + title: "ChatgptAuthTokensLoginAccountParams", + description: + "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", + }), + Schema.Struct({ + apiKey: Schema.String, + region: Schema.String, + type: Schema.Literal("amazonBedrock").annotate({ + title: "AmazonBedrockLoginAccountParamsType", + }), + }).annotate({ + title: "AmazonBedrockLoginAccountParams", + description: "[UNSTABLE] Managed Amazon Bedrock login is experimental.", + }), + ], + { mode: "oneOf" }, +); + export type ClientRequest__ListMcpServerStatusParams = { readonly cursor?: string | null; readonly detail?: ClientRequest__McpServerStatusDetail | null; @@ -10616,8 +11366,10 @@ export type ClientRequest__MigrationDetails = { readonly commands?: ReadonlyArray; readonly hooks?: ReadonlyArray; readonly mcpServers?: ReadonlyArray; + readonly memory?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const ClientRequest__MigrationDetails = Schema.Struct({ @@ -10628,12 +11380,14 @@ export const ClientRequest__MigrationDetails = Schema.Struct({ mcpServers: Schema.optionalKey( Schema.Array(ClientRequest__McpServerMigration).annotate({ default: [] }), ), + memory: Schema.optionalKey(Schema.Array(Schema.String)), plugins: Schema.optionalKey( Schema.Array(ClientRequest__PluginsMigration).annotate({ default: [] }), ), sessions: Schema.optionalKey( Schema.Array(ClientRequest__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey(Schema.Array(ClientRequest__SkillMigration).annotate({ default: [] })), subagents: Schema.optionalKey( Schema.Array(ClientRequest__SubagentMigration).annotate({ default: [] }), ), @@ -10655,6 +11409,8 @@ export type ClientRequest__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const ClientRequest__UserInput = Schema.Union( @@ -10679,6 +11435,14 @@ export const ClientRequest__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -10730,6 +11494,7 @@ export type ClientRequest__ThreadForkParams = { readonly cwd?: string | null; readonly developerInstructions?: string | null; readonly ephemeral?: boolean; + readonly lastTurnId?: string | null; readonly model?: string | null; readonly modelProvider?: string | null; readonly sandbox?: ClientRequest__SandboxMode | null; @@ -10752,6 +11517,15 @@ export const ClientRequest__ThreadForkParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), developerInstructions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), ephemeral: Schema.optionalKey(Schema.Boolean), + lastTurnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.", + }), + Schema.Null, + ]), + ), model: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -10965,27 +11739,47 @@ export const CommandExecutionRequestApprovalParams__CommandAction = Schema.Union { mode: "oneOf" }, ); -export type CommandExecutionRequestApprovalParams__FileSystemPath = - | { readonly path: CommandExecutionRequestApprovalParams__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } +export type CommandExecutionRequestApprovalParams__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } | { - readonly type: "special"; - readonly value: CommandExecutionRequestApprovalParams__FileSystemSpecialPath; + readonly kind: "project_roots"; + readonly subpath?: CommandExecutionRequestApprovalParams__LegacyAppPathString | null; + } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: CommandExecutionRequestApprovalParams__LegacyAppPathString | null; }; -export const CommandExecutionRequestApprovalParams__FileSystemPath = Schema.Union( +export const CommandExecutionRequestApprovalParams__FileSystemSpecialPath = Schema.Union( [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), Schema.Struct({ - path: CommandExecutionRequestApprovalParams__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([CommandExecutionRequestApprovalParams__LegacyAppPathString, Schema.Null]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: CommandExecutionRequestApprovalParams__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([CommandExecutionRequestApprovalParams__LegacyAppPathString, Schema.Null]), + ), + }), ], { mode: "oneOf" }, ); @@ -11263,52 +12057,92 @@ export const McpServerElicitationRequestParams__McpElicitationUntitledSingleSele type: McpServerElicitationRequestParams__McpElicitationStringType, }); -export type PermissionsRequestApprovalParams__FileSystemPath = - | { readonly path: PermissionsRequestApprovalParams__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } +export type PermissionsRequestApprovalParams__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } | { - readonly type: "special"; - readonly value: PermissionsRequestApprovalParams__FileSystemSpecialPath; + readonly kind: "project_roots"; + readonly subpath?: PermissionsRequestApprovalParams__LegacyAppPathString | null; + } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: PermissionsRequestApprovalParams__LegacyAppPathString | null; }; -export const PermissionsRequestApprovalParams__FileSystemPath = Schema.Union( +export const PermissionsRequestApprovalParams__FileSystemSpecialPath = Schema.Union( [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), Schema.Struct({ - path: PermissionsRequestApprovalParams__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalParams__LegacyAppPathString, Schema.Null]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: PermissionsRequestApprovalParams__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalParams__LegacyAppPathString, Schema.Null]), + ), + }), ], { mode: "oneOf" }, ); -export type PermissionsRequestApprovalResponse__FileSystemPath = - | { readonly path: PermissionsRequestApprovalResponse__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } +export type PermissionsRequestApprovalResponse__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } | { - readonly type: "special"; - readonly value: PermissionsRequestApprovalResponse__FileSystemSpecialPath; + readonly kind: "project_roots"; + readonly subpath?: PermissionsRequestApprovalResponse__LegacyAppPathString | null; + } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: PermissionsRequestApprovalResponse__LegacyAppPathString | null; }; -export const PermissionsRequestApprovalResponse__FileSystemPath = Schema.Union( +export const PermissionsRequestApprovalResponse__FileSystemSpecialPath = Schema.Union( [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), Schema.Struct({ - path: PermissionsRequestApprovalResponse__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalResponse__LegacyAppPathString, Schema.Null]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: PermissionsRequestApprovalResponse__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalResponse__LegacyAppPathString, Schema.Null]), + ), + }), ], { mode: "oneOf" }, ); @@ -11451,27 +12285,37 @@ export const ServerNotification__CollabAgentState = Schema.Struct({ status: ServerNotification__CollabAgentStatus, }); -export type ServerNotification__FileSystemPath = - | { readonly path: ServerNotification__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } - | { readonly type: "special"; readonly value: ServerNotification__FileSystemSpecialPath }; -export const ServerNotification__FileSystemPath = Schema.Union( - [ - Schema.Struct({ - path: ServerNotification__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), - Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: ServerNotification__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), - ], - { mode: "oneOf" }, -); +export type ServerNotification__ExternalAgentConfigImportItemTypeFailure = { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + readonly subErrorType?: string | null; +}; +export const ServerNotification__ExternalAgentConfigImportItemTypeFailure = Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + subErrorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); + +export type ServerNotification__ExternalAgentConfigImportItemTypeSuccess = { + readonly cwd?: string | null; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; +}; +export const ServerNotification__ExternalAgentConfigImportItemTypeSuccess = Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +}); export type ServerNotification__FuzzyFileSearchResult = { readonly file_name: string; @@ -11528,14 +12372,63 @@ export const ServerNotification__HookOutputEntry = Schema.Struct({ text: Schema.String, }); +export type ServerNotification__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } + | { + readonly kind: "project_roots"; + readonly subpath?: ServerNotification__LegacyAppPathString | null; + } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: ServerNotification__LegacyAppPathString | null; + }; +export const ServerNotification__FileSystemSpecialPath = Schema.Union( + [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([ServerNotification__LegacyAppPathString, Schema.Null]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([ServerNotification__LegacyAppPathString, Schema.Null]), + ), + }), + ], + { mode: "oneOf" }, +); + export type ServerNotification__McpServerStatusUpdatedNotification = { readonly error?: string | null; + readonly failureReason?: ServerNotification__McpServerStartupFailureReason | null; readonly name: string; readonly status: ServerNotification__McpServerStartupState; readonly threadId?: string | null; }; export const ServerNotification__McpServerStatusUpdatedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureReason: Schema.optionalKey( + Schema.Union([ServerNotification__McpServerStartupFailureReason, Schema.Null]), + ), name: Schema.String, status: ServerNotification__McpServerStartupState, threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -11578,6 +12471,7 @@ export const ServerNotification__ModelVerificationNotification = Schema.Struct({ export type ServerNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -11600,6 +12494,7 @@ export const ServerNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -11759,6 +12654,7 @@ export type ServerNotification__RateLimitSnapshot = { readonly primary?: ServerNotification__RateLimitWindow | null; readonly rateLimitReachedType?: ServerNotification__RateLimitReachedType | null; readonly secondary?: ServerNotification__RateLimitWindow | null; + readonly spendControlReached?: boolean | null; }; export const ServerNotification__RateLimitSnapshot = Schema.Struct({ credits: Schema.optionalKey(Schema.Union([ServerNotification__CreditsSnapshot, Schema.Null])), @@ -11773,6 +12669,15 @@ export const ServerNotification__RateLimitSnapshot = Schema.Struct({ Schema.Union([ServerNotification__RateLimitReachedType, Schema.Null]), ), secondary: Schema.optionalKey(Schema.Union([ServerNotification__RateLimitWindow, Schema.Null])), + spendControlReached: Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + description: + "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + }), + Schema.Null, + ]), + ), }); export type ServerNotification__UserInput = @@ -11791,6 +12696,8 @@ export type ServerNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const ServerNotification__UserInput = Schema.Union( @@ -11815,6 +12722,14 @@ export const ServerNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -12020,24 +12935,40 @@ export const ServerRequest__ChatgptAuthTokensRefreshParams = Schema.Struct({ reason: ServerRequest__ChatgptAuthTokensRefreshReason, }); -export type ServerRequest__FileSystemPath = - | { readonly path: ServerRequest__AbsolutePathBuf; readonly type: "path" } - | { readonly pattern: string; readonly type: "glob_pattern" } - | { readonly type: "special"; readonly value: ServerRequest__FileSystemSpecialPath }; -export const ServerRequest__FileSystemPath = Schema.Union( +export type ServerRequest__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } + | { readonly kind: "project_roots"; readonly subpath?: ServerRequest__LegacyAppPathString | null } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: ServerRequest__LegacyAppPathString | null; + }; +export const ServerRequest__FileSystemSpecialPath = Schema.Union( [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), Schema.Struct({ - path: ServerRequest__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey(Schema.Union([ServerRequest__LegacyAppPathString, Schema.Null])), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: ServerRequest__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey(Schema.Union([ServerRequest__LegacyAppPathString, Schema.Null])), + }), ], { mode: "oneOf" }, ); @@ -12317,6 +13248,7 @@ export type V2AccountRateLimitsUpdatedNotification__RateLimitSnapshot = { readonly primary?: V2AccountRateLimitsUpdatedNotification__RateLimitWindow | null; readonly rateLimitReachedType?: V2AccountRateLimitsUpdatedNotification__RateLimitReachedType | null; readonly secondary?: V2AccountRateLimitsUpdatedNotification__RateLimitWindow | null; + readonly spendControlReached?: boolean | null; }; export const V2AccountRateLimitsUpdatedNotification__RateLimitSnapshot = Schema.Struct({ credits: Schema.optionalKey( @@ -12339,6 +13271,15 @@ export const V2AccountRateLimitsUpdatedNotification__RateLimitSnapshot = Schema. secondary: Schema.optionalKey( Schema.Union([V2AccountRateLimitsUpdatedNotification__RateLimitWindow, Schema.Null]), ), + spendControlReached: Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + description: + "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + }), + Schema.Null, + ]), + ), }); export type V2AppListUpdatedNotification__AppMetadata = { @@ -12403,6 +13344,23 @@ export const V2AppsListResponse__AppMetadata = Schema.Struct({ versionNotes: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2AppsReadResponse__ConnectorMetadata = { + readonly description?: string | null; + readonly iconUrl?: string | null; + readonly id: string; + readonly name: string; + readonly toolSummaries?: ReadonlyArray | null; +}; +export const V2AppsReadResponse__ConnectorMetadata = Schema.Struct({ + description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.String, + name: Schema.String, + toolSummaries: Schema.optionalKey( + Schema.Union([Schema.Array(V2AppsReadResponse__AppToolSummary), Schema.Null]), + ), +}).annotate({ description: "EXPERIMENTAL - metadata returned by app/read." }); + export type V2CommandExecParams__SandboxPolicy = | { readonly type: "dangerFullAccess" } | { readonly networkAccess?: boolean; readonly type: "readOnly" } @@ -12555,6 +13513,25 @@ export const V2ConfigReadResponse__ConfigLayerSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ConfigReadResponse__AppsDefaultConfig = { + readonly approvals_reviewer?: V2ConfigReadResponse__ApprovalsReviewer | null; + readonly default_tools_approval_mode?: V2ConfigReadResponse__AppToolApproval | null; + readonly destructive_enabled?: boolean; + readonly enabled?: boolean; + readonly open_world_enabled?: boolean; +}; +export const V2ConfigReadResponse__AppsDefaultConfig = Schema.Struct({ + approvals_reviewer: Schema.optionalKey( + Schema.Union([V2ConfigReadResponse__ApprovalsReviewer, Schema.Null]), + ), + default_tools_approval_mode: Schema.optionalKey( + Schema.Union([V2ConfigReadResponse__AppToolApproval, Schema.Null]), + ), + destructive_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), + enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), + open_world_enabled: Schema.optionalKey(Schema.Boolean.annotate({ default: true })), +}); + export type V2ConfigReadResponse__WebSearchToolConfig = { readonly allowed_domains?: ReadonlyArray | null; readonly context_size?: V2ConfigReadResponse__WebSearchContextSize | null; @@ -12579,50 +13556,17 @@ export const V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup = Sche matcher: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); -export type V2ConfigRequirementsReadResponse__ConfigRequirements = { - readonly allowAppshots?: boolean | null; - readonly allowManagedHooksOnly?: boolean | null; - readonly allowedApprovalPolicies?: ReadonlyArray | null; - readonly allowedPermissionProfiles?: { readonly [x: string]: boolean } | null; - readonly allowedSandboxModes?: ReadonlyArray | null; - readonly allowedWebSearchModes?: ReadonlyArray | null; - readonly allowedWindowsSandboxImplementations?: ReadonlyArray | null; - readonly computerUse?: V2ConfigRequirementsReadResponse__ComputerUseRequirements | null; - readonly defaultPermissions?: string | null; - readonly enforceResidency?: V2ConfigRequirementsReadResponse__ResidencyRequirement | null; - readonly featureRequirements?: { readonly [x: string]: boolean } | null; +export type V2ConfigRequirementsReadResponse__NewThreadModelDefaults = { + readonly model?: string | null; + readonly modelReasoningEffort?: V2ConfigRequirementsReadResponse__ReasoningEffort | null; + readonly serviceTier?: string | null; }; -export const V2ConfigRequirementsReadResponse__ConfigRequirements = Schema.Struct({ - allowAppshots: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - allowManagedHooksOnly: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - allowedApprovalPolicies: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__AskForApproval), Schema.Null]), - ), - allowedPermissionProfiles: Schema.optionalKey( - Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), - ), - allowedSandboxModes: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__SandboxMode), Schema.Null]), - ), - allowedWebSearchModes: Schema.optionalKey( - Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__WebSearchMode), Schema.Null]), - ), - allowedWindowsSandboxImplementations: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ConfigRequirementsReadResponse__WindowsSandboxSetupMode), - Schema.Null, - ]), - ), - computerUse: Schema.optionalKey( - Schema.Union([V2ConfigRequirementsReadResponse__ComputerUseRequirements, Schema.Null]), - ), - defaultPermissions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - enforceResidency: Schema.optionalKey( - Schema.Union([V2ConfigRequirementsReadResponse__ResidencyRequirement, Schema.Null]), - ), - featureRequirements: Schema.optionalKey( - Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), +export const V2ConfigRequirementsReadResponse__NewThreadModelDefaults = Schema.Struct({ + model: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + modelReasoningEffort: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ReasoningEffort, Schema.Null]), ), + serviceTier: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); export type V2ConfigWarningNotification__TextRange = { @@ -12734,6 +13678,7 @@ export const V2ConfigWriteResponse__ConfigLayerSource = Schema.Union( export type V2ErrorNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -12756,6 +13701,7 @@ export const V2ErrorNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -12844,8 +13790,10 @@ export type V2ExternalAgentConfigDetectResponse__MigrationDetails = { readonly commands?: ReadonlyArray; readonly hooks?: ReadonlyArray; readonly mcpServers?: ReadonlyArray; + readonly memory?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const V2ExternalAgentConfigDetectResponse__MigrationDetails = Schema.Struct({ @@ -12858,23 +13806,120 @@ export const V2ExternalAgentConfigDetectResponse__MigrationDetails = Schema.Stru mcpServers: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__McpServerMigration).annotate({ default: [] }), ), + memory: Schema.optionalKey(Schema.Array(Schema.String)), plugins: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__PluginsMigration).annotate({ default: [] }), ), sessions: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey( + Schema.Array(V2ExternalAgentConfigDetectResponse__SkillMigration).annotate({ default: [] }), + ), subagents: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigDetectResponse__SubagentMigration).annotate({ default: [] }), ), }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + readonly subErrorType?: string | null; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + subErrorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + readonly subErrorType?: string | null; + }; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + subErrorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorCandidate = + { + readonly name: string; + readonly sessionCount: number; + readonly source: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorSource; + }; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorCandidate = + Schema.Struct({ + name: Schema.String, + sessionCount: Schema.Number.annotate({ format: "uint32" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + source: V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorSource, + }); + export type V2ExternalAgentConfigImportParams__MigrationDetails = { readonly commands?: ReadonlyArray; readonly hooks?: ReadonlyArray; readonly mcpServers?: ReadonlyArray; + readonly memory?: ReadonlyArray; readonly plugins?: ReadonlyArray; readonly sessions?: ReadonlyArray; + readonly skills?: ReadonlyArray; readonly subagents?: ReadonlyArray; }; export const V2ExternalAgentConfigImportParams__MigrationDetails = Schema.Struct({ @@ -12887,17 +13932,57 @@ export const V2ExternalAgentConfigImportParams__MigrationDetails = Schema.Struct mcpServers: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__McpServerMigration).annotate({ default: [] }), ), + memory: Schema.optionalKey(Schema.Array(Schema.String)), plugins: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__PluginsMigration).annotate({ default: [] }), ), sessions: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__SessionMigration).annotate({ default: [] }), ), + skills: Schema.optionalKey( + Schema.Array(V2ExternalAgentConfigImportParams__SkillMigration).annotate({ default: [] }), + ), subagents: Schema.optionalKey( Schema.Array(V2ExternalAgentConfigImportParams__SubagentMigration).annotate({ default: [] }), ), }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure = + { + readonly cwd?: string | null; + readonly errorType?: string | null; + readonly failureStage: string; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly message: string; + readonly source?: string | null; + readonly subErrorType?: string | null; + }; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + errorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureStage: Schema.String, + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + message: Schema.String, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + subErrorType: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess = + { + readonly cwd?: string | null; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly source?: string | null; + readonly target?: string | null; + }; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess = + Schema.Struct({ + cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + source: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + target: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + }); + export type V2FileChangePatchUpdatedNotification__FileUpdateChange = { readonly diff: string; readonly kind: V2FileChangePatchUpdatedNotification__PatchChangeKind; @@ -12909,6 +13994,52 @@ export const V2FileChangePatchUpdatedNotification__FileUpdateChange = Schema.Str path: Schema.String, }); +export type V2GetAccountRateLimitsResponse__RateLimitResetCredit = { + readonly description?: string | null; + readonly expiresAt?: number | null; + readonly grantedAt: number; + readonly id: string; + readonly resetType: V2GetAccountRateLimitsResponse__RateLimitResetType; + readonly status: V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus; + readonly title?: string | null; +}; +export const V2GetAccountRateLimitsResponse__RateLimitResetCredit = Schema.Struct({ + description: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Backend-provided display description for this credit, or `null` when unavailable.", + }), + Schema.Null, + ]), + ), + expiresAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: + "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + grantedAt: Schema.Number.annotate({ + description: "Unix timestamp in seconds when the credit was granted.", + format: "int64", + }).check(Schema.isInt()), + id: Schema.String.annotate({ description: "Opaque backend identifier for this reset credit." }), + resetType: V2GetAccountRateLimitsResponse__RateLimitResetType, + status: V2GetAccountRateLimitsResponse__RateLimitResetCreditStatus, + title: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Backend-provided display title for this credit, or `null` when unavailable.", + }), + Schema.Null, + ]), + ), +}); + export type V2GetAccountRateLimitsResponse__RateLimitSnapshot = { readonly credits?: V2GetAccountRateLimitsResponse__CreditsSnapshot | null; readonly individualLimit?: V2GetAccountRateLimitsResponse__SpendControlLimitSnapshot | null; @@ -12918,6 +14049,7 @@ export type V2GetAccountRateLimitsResponse__RateLimitSnapshot = { readonly primary?: V2GetAccountRateLimitsResponse__RateLimitWindow | null; readonly rateLimitReachedType?: V2GetAccountRateLimitsResponse__RateLimitReachedType | null; readonly secondary?: V2GetAccountRateLimitsResponse__RateLimitWindow | null; + readonly spendControlReached?: boolean | null; }; export const V2GetAccountRateLimitsResponse__RateLimitSnapshot = Schema.Struct({ credits: Schema.optionalKey( @@ -12940,33 +14072,74 @@ export const V2GetAccountRateLimitsResponse__RateLimitSnapshot = Schema.Struct({ secondary: Schema.optionalKey( Schema.Union([V2GetAccountRateLimitsResponse__RateLimitWindow, Schema.Null]), ), + spendControlReached: Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + description: + "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + }), + Schema.Null, + ]), + ), }); export type V2GetAccountResponse__Account = | { readonly type: "apiKey" } | { - readonly email: string; + readonly email: string | null; readonly planType: V2GetAccountResponse__PlanType; readonly type: "chatgpt"; } - | { readonly type: "amazonBedrock" }; + | { readonly type: "amazonBedrock"; readonly usesCodexManagedCredentials?: boolean }; export const V2GetAccountResponse__Account = Schema.Union( [ Schema.Struct({ type: Schema.Literal("apiKey").annotate({ title: "ApiKeyAccountType" }), }).annotate({ title: "ApiKeyAccount" }), Schema.Struct({ - email: Schema.String, + email: Schema.Union([Schema.String, Schema.Null]), planType: V2GetAccountResponse__PlanType, type: Schema.Literal("chatgpt").annotate({ title: "ChatgptAccountType" }), }).annotate({ title: "ChatgptAccount" }), Schema.Struct({ type: Schema.Literal("amazonBedrock").annotate({ title: "AmazonBedrockAccountType" }), + usesCodexManagedCredentials: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), }).annotate({ title: "AmazonBedrockAccount" }), ], { mode: "oneOf" }, ); +export type V2GetWorkspaceMessagesResponse__WorkspaceMessage = { + readonly archivedAt?: number | null; + readonly createdAt?: number | null; + readonly messageBody: string; + readonly messageId: string; + readonly messageType: V2GetWorkspaceMessagesResponse__WorkspaceMessageType; +}; +export const V2GetWorkspaceMessagesResponse__WorkspaceMessage = Schema.Struct({ + archivedAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) when the message was archived.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + createdAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) when the message was created.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), + messageBody: Schema.String, + messageId: Schema.String, + messageType: V2GetWorkspaceMessagesResponse__WorkspaceMessageType, +}); + export type V2HookCompletedNotification__HookOutputEntry = { readonly kind: V2HookCompletedNotification__HookOutputEntryKind; readonly text: string; @@ -13109,6 +14282,8 @@ export type V2ItemCompletedNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ItemCompletedNotification__UserInput = Schema.Union( @@ -13137,6 +14312,14 @@ export const V2ItemCompletedNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -13151,34 +14334,6 @@ export const V2ItemCompletedNotification__UserInput = Schema.Union( { mode: "oneOf" }, ); -export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = - | { - readonly path: V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf; - readonly type: "path"; - } - | { readonly pattern: string; readonly type: "glob_pattern" } - | { - readonly type: "special"; - readonly value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath; - }; -export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = Schema.Union( - [ - Schema.Struct({ - path: V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), - Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), - ], - { mode: "oneOf" }, -); - export type V2ItemGuardianApprovalReviewCompletedNotification__GuardianApprovalReview = { readonly rationale?: string | null; readonly riskLevel?: V2ItemGuardianApprovalReviewCompletedNotification__GuardianRiskLevel | null; @@ -13206,33 +14361,57 @@ export const V2ItemGuardianApprovalReviewCompletedNotification__GuardianApproval "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", }); -export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = +export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } | { - readonly path: V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf; - readonly type: "path"; + readonly kind: "project_roots"; + readonly subpath?: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString | null; } - | { readonly pattern: string; readonly type: "glob_pattern" } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } | { - readonly type: "special"; - readonly value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath; + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString | null; }; -export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = Schema.Union( - [ - Schema.Struct({ - path: V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf, - type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), - }).annotate({ title: "PathFileSystemPath" }), - Schema.Struct({ - pattern: Schema.String, - type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), - }).annotate({ title: "GlobPatternFileSystemPath" }), - Schema.Struct({ - type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), - value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath, - }).annotate({ title: "SpecialFileSystemPath" }), - ], - { mode: "oneOf" }, -); +export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath = + Schema.Union( + [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + Schema.Null, + ]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + Schema.Null, + ]), + ), + }), + ], + { mode: "oneOf" }, + ); export type V2ItemGuardianApprovalReviewStartedNotification__GuardianApprovalReview = { readonly rationale?: string | null; @@ -13261,6 +14440,57 @@ export const V2ItemGuardianApprovalReviewStartedNotification__GuardianApprovalRe "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", }); +export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath = + | { readonly kind: "root" } + | { readonly kind: "minimal" } + | { + readonly kind: "project_roots"; + readonly subpath?: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString | null; + } + | { readonly kind: "tmpdir" } + | { readonly kind: "slash_tmp" } + | { + readonly kind: "unknown"; + readonly path: string; + readonly subpath?: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString | null; + }; +export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath = Schema.Union( + [ + Schema.Struct({ kind: Schema.Literal("root") }).annotate({ + title: "RootFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("minimal") }).annotate({ + title: "MinimalFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("project_roots"), + subpath: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString, + Schema.Null, + ]), + ), + }).annotate({ title: "KindFileSystemSpecialPath" }), + Schema.Struct({ kind: Schema.Literal("tmpdir") }).annotate({ + title: "TmpdirFileSystemSpecialPath", + }), + Schema.Struct({ kind: Schema.Literal("slash_tmp") }).annotate({ + title: "SlashTmpFileSystemSpecialPath", + }), + Schema.Struct({ + kind: Schema.Literal("unknown"), + path: Schema.String, + subpath: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString, + Schema.Null, + ]), + ), + }), + ], + { mode: "oneOf" }, +); + export type V2ItemStartedNotification__CommandAction = | { readonly command: string; @@ -13348,6 +14578,8 @@ export type V2ItemStartedNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ItemStartedNotification__UserInput = Schema.Union( @@ -13376,6 +14608,14 @@ export const V2ItemStartedNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -13437,7 +14677,9 @@ export type V2PluginInstalledResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginInstalledResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginInstalledResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13477,12 +14719,23 @@ export const V2PluginInstalledResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginInstalledResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13505,6 +14758,12 @@ export type V2PluginInstalledResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginInstalledResponse__PluginSource = Schema.Union( [ @@ -13519,6 +14778,25 @@ export const V2PluginInstalledResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13562,7 +14840,9 @@ export type V2PluginListResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginListResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginListResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13602,12 +14882,23 @@ export const V2PluginListResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginListResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13630,6 +14921,12 @@ export type V2PluginListResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginListResponse__PluginSource = Schema.Union( [ @@ -13644,6 +14941,25 @@ export const V2PluginListResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13678,7 +14994,9 @@ export type V2PluginReadResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginReadResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginReadResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13718,12 +15036,23 @@ export const V2PluginReadResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginReadResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13746,6 +15075,12 @@ export type V2PluginReadResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginReadResponse__PluginSource = Schema.Union( [ @@ -13760,6 +15095,25 @@ export const V2PluginReadResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13790,6 +15144,7 @@ export const V2PluginReadResponse__SkillInterface = Schema.Struct({ export type V2PluginReadResponse__AppTemplateSummary = { readonly canonicalConnectorId?: string | null; + readonly category?: string | null; readonly description?: string | null; readonly logoUrl?: string | null; readonly logoUrlDark?: string | null; @@ -13800,6 +15155,7 @@ export type V2PluginReadResponse__AppTemplateSummary = { }; export const V2PluginReadResponse__AppTemplateSummary = Schema.Struct({ canonicalConnectorId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + category: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), logoUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), logoUrlDark: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -13833,6 +15189,47 @@ export const V2PluginReadResponse__PluginSharePrincipal = Schema.Struct({ role: V2PluginReadResponse__PluginSharePrincipalRole, }); +export type V2PluginReadResponse__ScheduledTaskSchedule = + | { + readonly days?: ReadonlyArray | null; + readonly intervalHours: number; + readonly type: "hourly"; + } + | { readonly time: string; readonly type: "daily" } + | { readonly time: string; readonly type: "weekdays" } + | { + readonly days: ReadonlyArray; + readonly time: string; + readonly type: "weekly"; + }; +export const V2PluginReadResponse__ScheduledTaskSchedule = Schema.Union( + [ + Schema.Struct({ + days: Schema.optionalKey( + Schema.Union([Schema.Array(V2PluginReadResponse__ScheduledTaskWeekday), Schema.Null]), + ), + intervalHours: Schema.Number.annotate({ format: "uint32" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + type: Schema.Literal("hourly").annotate({ title: "HourlyScheduledTaskScheduleType" }), + }).annotate({ title: "HourlyScheduledTaskSchedule" }), + Schema.Struct({ + time: Schema.String, + type: Schema.Literal("daily").annotate({ title: "DailyScheduledTaskScheduleType" }), + }).annotate({ title: "DailyScheduledTaskSchedule" }), + Schema.Struct({ + time: Schema.String, + type: Schema.Literal("weekdays").annotate({ title: "WeekdaysScheduledTaskScheduleType" }), + }).annotate({ title: "WeekdaysScheduledTaskSchedule" }), + Schema.Struct({ + days: Schema.Array(V2PluginReadResponse__ScheduledTaskWeekday), + time: Schema.String, + type: Schema.Literal("weekly").annotate({ title: "WeeklyScheduledTaskScheduleType" }), + }).annotate({ title: "WeeklyScheduledTaskSchedule" }), + ], + { mode: "oneOf" }, +); + export type V2PluginShareListResponse__PluginInterface = { readonly brandColor?: string | null; readonly capabilities: ReadonlyArray; @@ -13843,7 +15240,9 @@ export type V2PluginShareListResponse__PluginInterface = { readonly developerName?: string | null; readonly displayName?: string | null; readonly logo?: V2PluginShareListResponse__AbsolutePathBuf | null; + readonly logoDark?: V2PluginShareListResponse__AbsolutePathBuf | null; readonly logoUrl?: string | null; + readonly logoUrlDark?: string | null; readonly longDescription?: string | null; readonly privacyPolicyUrl?: string | null; readonly screenshotUrls: ReadonlyArray; @@ -13883,12 +15282,23 @@ export const V2PluginShareListResponse__PluginInterface = Schema.Struct({ description: "Local logo path, resolved from the installed plugin package.", }), ), + logoDark: Schema.optionalKey( + Schema.Union([V2PluginShareListResponse__AbsolutePathBuf, Schema.Null]).annotate({ + description: "Local dark-mode logo path, resolved from the installed plugin package.", + }), + ), logoUrl: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ description: "Remote logo URL from the plugin catalog." }), Schema.Null, ]), ), + logoUrlDark: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Remote dark-mode logo URL from the plugin catalog." }), + Schema.Null, + ]), + ), longDescription: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), privacyPolicyUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), screenshotUrls: Schema.Array(Schema.String).annotate({ @@ -13911,6 +15321,12 @@ export type V2PluginShareListResponse__PluginSource = readonly type: "git"; readonly url: string; } + | { + readonly package: string; + readonly registry?: string | null; + readonly type: "npm"; + readonly version?: string | null; + } | { readonly type: "remote" }; export const V2PluginShareListResponse__PluginSource = Schema.Union( [ @@ -13925,6 +15341,25 @@ export const V2PluginShareListResponse__PluginSource = Schema.Union( type: Schema.Literal("git").annotate({ title: "GitPluginSourceType" }), url: Schema.String, }).annotate({ title: "GitPluginSource" }), + Schema.Struct({ + package: Schema.String, + registry: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + }), + Schema.Null, + ]), + ), + type: Schema.Literal("npm").annotate({ title: "NpmPluginSourceType" }), + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Optional npm version or version range." }), + Schema.Null, + ]), + ), + }).annotate({ title: "NpmPluginSource" }), Schema.Struct({ type: Schema.Literal("remote").annotate({ title: "RemotePluginSourceType" }), }).annotate({ @@ -13991,6 +15426,7 @@ export type V2RawResponseItemCompletedNotification__ContentItem = readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly text: string; readonly type: "output_text" }; export const V2RawResponseItemCompletedNotification__ContentItem = Schema.Union( [ @@ -14005,6 +15441,10 @@ export const V2RawResponseItemCompletedNotification__ContentItem = Schema.Union( image_url: Schema.String, type: Schema.Literal("input_image").annotate({ title: "InputImageContentItemType" }), }).annotate({ title: "InputImageContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ title: "InputAudioContentItemType" }), + }).annotate({ title: "InputAudioContentItem" }), Schema.Struct({ text: Schema.String, type: Schema.Literal("output_text").annotate({ title: "OutputTextContentItemType" }), @@ -14020,6 +15460,7 @@ export type V2RawResponseItemCompletedNotification__FunctionCallOutputContentIte readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2RawResponseItemCompletedNotification__FunctionCallOutputContentItem = Schema.Union( [ @@ -14038,6 +15479,12 @@ export const V2RawResponseItemCompletedNotification__FunctionCallOutputContentIt title: "InputImageFunctionCallOutputContentItemType", }), }).annotate({ title: "InputImageFunctionCallOutputContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ + title: "InputAudioFunctionCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioFunctionCallOutputContentItem" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -14113,6 +15560,7 @@ export const V2ReviewStartResponse__MemoryCitation = Schema.Struct({ export type V2ReviewStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14135,6 +15583,7 @@ export const V2ReviewStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14246,6 +15695,8 @@ export type V2ReviewStartResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ReviewStartResponse__UserInput = Schema.Union( @@ -14270,6 +15721,14 @@ export const V2ReviewStartResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -14370,6 +15829,7 @@ export const V2ThreadForkResponse__MemoryCitation = Schema.Struct({ export type V2ThreadForkResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14392,6 +15852,7 @@ export const V2ThreadForkResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14503,6 +15964,8 @@ export type V2ThreadForkResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadForkResponse__UserInput = Schema.Union( @@ -14527,6 +15990,14 @@ export const V2ThreadForkResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -14705,6 +16176,7 @@ export const V2ThreadListResponse__MemoryCitation = Schema.Struct({ export type V2ThreadListResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14727,6 +16199,7 @@ export const V2ThreadListResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -14838,6 +16311,8 @@ export type V2ThreadListResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadListResponse__UserInput = Schema.Union( @@ -14862,6 +16337,14 @@ export const V2ThreadListResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -14971,6 +16454,7 @@ export const V2ThreadMetadataUpdateResponse__MemoryCitation = Schema.Struct({ export type V2ThreadMetadataUpdateResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -14993,6 +16477,7 @@ export const V2ThreadMetadataUpdateResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15104,6 +16589,8 @@ export type V2ThreadMetadataUpdateResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadMetadataUpdateResponse__UserInput = Schema.Union( @@ -15132,6 +16619,14 @@ export const V2ThreadMetadataUpdateResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -15241,6 +16736,7 @@ export const V2ThreadReadResponse__MemoryCitation = Schema.Struct({ export type V2ThreadReadResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15263,6 +16759,7 @@ export const V2ThreadReadResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15374,6 +16871,8 @@ export type V2ThreadReadResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadReadResponse__UserInput = Schema.Union( @@ -15398,6 +16897,14 @@ export const V2ThreadReadResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -15452,6 +16959,7 @@ export type V2ThreadResumeParams__ContentItem = readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly text: string; readonly type: "output_text" }; export const V2ThreadResumeParams__ContentItem = Schema.Union( [ @@ -15464,6 +16972,10 @@ export const V2ThreadResumeParams__ContentItem = Schema.Union( image_url: Schema.String, type: Schema.Literal("input_image").annotate({ title: "InputImageContentItemType" }), }).annotate({ title: "InputImageContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ title: "InputAudioContentItemType" }), + }).annotate({ title: "InputAudioContentItem" }), Schema.Struct({ text: Schema.String, type: Schema.Literal("output_text").annotate({ title: "OutputTextContentItemType" }), @@ -15479,6 +16991,7 @@ export type V2ThreadResumeParams__FunctionCallOutputContentItem = readonly image_url: string; readonly type: "input_image"; } + | { readonly audio_url: string; readonly type: "input_audio" } | { readonly encrypted_content: string; readonly type: "encrypted_content" }; export const V2ThreadResumeParams__FunctionCallOutputContentItem = Schema.Union( [ @@ -15495,6 +17008,12 @@ export const V2ThreadResumeParams__FunctionCallOutputContentItem = Schema.Union( title: "InputImageFunctionCallOutputContentItemType", }), }).annotate({ title: "InputImageFunctionCallOutputContentItem" }), + Schema.Struct({ + audio_url: Schema.String, + type: Schema.Literal("input_audio").annotate({ + title: "InputAudioFunctionCallOutputContentItemType", + }), + }).annotate({ title: "InputAudioFunctionCallOutputContentItem" }), Schema.Struct({ encrypted_content: Schema.String, type: Schema.Literal("encrypted_content").annotate({ @@ -15570,6 +17089,7 @@ export const V2ThreadResumeResponse__MemoryCitation = Schema.Struct({ export type V2ThreadResumeResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15592,6 +17112,7 @@ export const V2ThreadResumeResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15703,6 +17224,8 @@ export type V2ThreadResumeResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadResumeResponse__UserInput = Schema.Union( @@ -15727,6 +17250,14 @@ export const V2ThreadResumeResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -15836,6 +17367,7 @@ export const V2ThreadRollbackResponse__MemoryCitation = Schema.Struct({ export type V2ThreadRollbackResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -15858,6 +17390,7 @@ export const V2ThreadRollbackResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -15969,6 +17502,8 @@ export type V2ThreadRollbackResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadRollbackResponse__UserInput = Schema.Union( @@ -15997,6 +17532,14 @@ export const V2ThreadRollbackResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -16164,6 +17707,7 @@ export const V2ThreadStartedNotification__MemoryCitation = Schema.Struct({ export type V2ThreadStartedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16186,6 +17730,7 @@ export const V2ThreadStartedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16297,6 +17842,8 @@ export type V2ThreadStartedNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadStartedNotification__UserInput = Schema.Union( @@ -16325,6 +17872,14 @@ export const V2ThreadStartedNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -16434,6 +17989,7 @@ export const V2ThreadStartResponse__MemoryCitation = Schema.Struct({ export type V2ThreadStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16456,6 +18012,7 @@ export const V2ThreadStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16567,6 +18124,8 @@ export type V2ThreadStartResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadStartResponse__UserInput = Schema.Union( @@ -16591,6 +18150,14 @@ export const V2ThreadStartResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -16740,6 +18307,7 @@ export const V2ThreadUnarchiveResponse__MemoryCitation = Schema.Struct({ export type V2ThreadUnarchiveResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -16762,6 +18330,7 @@ export const V2ThreadUnarchiveResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -16873,6 +18442,8 @@ export type V2ThreadUnarchiveResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2ThreadUnarchiveResponse__UserInput = Schema.Union( @@ -16901,6 +18472,14 @@ export const V2ThreadUnarchiveResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -17010,6 +18589,7 @@ export const V2TurnCompletedNotification__MemoryCitation = Schema.Struct({ export type V2TurnCompletedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17032,6 +18612,7 @@ export const V2TurnCompletedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17143,6 +18724,8 @@ export type V2TurnCompletedNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2TurnCompletedNotification__UserInput = Schema.Union( @@ -17171,6 +18754,14 @@ export const V2TurnCompletedNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -17256,6 +18847,7 @@ export const V2TurnStartedNotification__MemoryCitation = Schema.Struct({ export type V2TurnStartedNotification__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17278,6 +18870,7 @@ export const V2TurnStartedNotification__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17389,6 +18982,8 @@ export type V2TurnStartedNotification__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2TurnStartedNotification__UserInput = Schema.Union( @@ -17417,6 +19012,14 @@ export const V2TurnStartedNotification__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -17503,6 +19106,8 @@ export type V2TurnStartParams__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2TurnStartParams__UserInput = Schema.Union( @@ -17527,6 +19132,14 @@ export const V2TurnStartParams__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -17603,6 +19216,7 @@ export const V2TurnStartResponse__MemoryCitation = Schema.Struct({ export type V2TurnStartResponse__CodexErrorInfo = | "contextWindowExceeded" + | "sessionBudgetExceeded" | "usageLimitExceeded" | "serverOverloaded" | "cyberPolicy" @@ -17625,6 +19239,7 @@ export const V2TurnStartResponse__CodexErrorInfo = Schema.Union( [ Schema.Literals([ "contextWindowExceeded", + "sessionBudgetExceeded", "usageLimitExceeded", "serverOverloaded", "cyberPolicy", @@ -17736,6 +19351,8 @@ export type V2TurnStartResponse__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2TurnStartResponse__UserInput = Schema.Union( @@ -17760,6 +19377,14 @@ export const V2TurnStartResponse__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -17790,6 +19415,8 @@ export type V2TurnSteerParams__UserInput = readonly path: string; readonly type: "localImage"; } + | { readonly type: "audio"; readonly url: string } + | { readonly path: string; readonly type: "localAudio" } | { readonly name: string; readonly path: string; readonly type: "skill" } | { readonly name: string; readonly path: string; readonly type: "mention" }; export const V2TurnSteerParams__UserInput = Schema.Union( @@ -17814,6 +19441,14 @@ export const V2TurnSteerParams__UserInput = Schema.Union( path: Schema.String, type: Schema.Literal("localImage").annotate({ title: "LocalImageUserInputType" }), }).annotate({ title: "LocalImageUserInput" }), + Schema.Struct({ + type: Schema.Literal("audio").annotate({ title: "AudioUserInputType" }), + url: Schema.String, + }).annotate({ title: "AudioUserInput" }), + Schema.Struct({ + path: Schema.String, + type: Schema.Literal("localAudio").annotate({ title: "LocalAudioUserInputType" }), + }).annotate({ title: "LocalAudioUserInput" }), Schema.Struct({ name: Schema.String, path: Schema.String, @@ -18179,14 +19814,33 @@ export const ClientRequest__TurnSteerParams = Schema.Struct({ threadId: Schema.String, }); -export type CommandExecutionRequestApprovalParams__FileSystemSandboxEntry = { - readonly access: CommandExecutionRequestApprovalParams__FileSystemAccessMode; - readonly path: CommandExecutionRequestApprovalParams__FileSystemPath; -}; -export const CommandExecutionRequestApprovalParams__FileSystemSandboxEntry = Schema.Struct({ - access: CommandExecutionRequestApprovalParams__FileSystemAccessMode, - path: CommandExecutionRequestApprovalParams__FileSystemPath, -}); +export type CommandExecutionRequestApprovalParams__FileSystemPath = + | { + readonly path: CommandExecutionRequestApprovalParams__LegacyAppPathString; + readonly type: "path"; + } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: CommandExecutionRequestApprovalParams__FileSystemSpecialPath; + }; +export const CommandExecutionRequestApprovalParams__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: CommandExecutionRequestApprovalParams__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: CommandExecutionRequestApprovalParams__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); export type CommandExecutionRequestApprovalResponse__CommandExecutionApprovalDecision = | "accept" @@ -18374,29 +20028,66 @@ export const McpServerElicitationRequestParams__McpElicitationSingleSelectEnumSc ], ); -export type PermissionsRequestApprovalParams__FileSystemSandboxEntry = { - readonly access: PermissionsRequestApprovalParams__FileSystemAccessMode; - readonly path: PermissionsRequestApprovalParams__FileSystemPath; -}; -export const PermissionsRequestApprovalParams__FileSystemSandboxEntry = Schema.Struct({ - access: PermissionsRequestApprovalParams__FileSystemAccessMode, - path: PermissionsRequestApprovalParams__FileSystemPath, -}); +export type PermissionsRequestApprovalParams__FileSystemPath = + | { readonly path: PermissionsRequestApprovalParams__LegacyAppPathString; readonly type: "path" } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: PermissionsRequestApprovalParams__FileSystemSpecialPath; + }; +export const PermissionsRequestApprovalParams__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: PermissionsRequestApprovalParams__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: PermissionsRequestApprovalParams__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); -export type PermissionsRequestApprovalResponse__FileSystemSandboxEntry = { - readonly access: PermissionsRequestApprovalResponse__FileSystemAccessMode; - readonly path: PermissionsRequestApprovalResponse__FileSystemPath; -}; -export const PermissionsRequestApprovalResponse__FileSystemSandboxEntry = Schema.Struct({ - access: PermissionsRequestApprovalResponse__FileSystemAccessMode, - path: PermissionsRequestApprovalResponse__FileSystemPath, -}); +export type PermissionsRequestApprovalResponse__FileSystemPath = + | { + readonly path: PermissionsRequestApprovalResponse__LegacyAppPathString; + readonly type: "path"; + } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: PermissionsRequestApprovalResponse__FileSystemSpecialPath; + }; +export const PermissionsRequestApprovalResponse__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: PermissionsRequestApprovalResponse__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: PermissionsRequestApprovalResponse__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); export type ServerNotification__AppInfo = { readonly appMetadata?: ServerNotification__AppMetadata | null; readonly branding?: ServerNotification__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -18412,6 +20103,12 @@ export const ServerNotification__AppInfo = Schema.Struct({ branding: Schema.optionalKey(Schema.Union([ServerNotification__AppBranding, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -18431,13 +20128,15 @@ export const ServerNotification__AppInfo = Schema.Struct({ pluginDisplayNames: Schema.optionalKey(Schema.Array(Schema.String).annotate({ default: [] })), }).annotate({ description: "EXPERIMENTAL - app metadata returned by app-list APIs." }); -export type ServerNotification__FileSystemSandboxEntry = { - readonly access: ServerNotification__FileSystemAccessMode; - readonly path: ServerNotification__FileSystemPath; +export type ServerNotification__ExternalAgentConfigImportTypeResult = { + readonly failures: ReadonlyArray; + readonly itemType: ServerNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; }; -export const ServerNotification__FileSystemSandboxEntry = Schema.Struct({ - access: ServerNotification__FileSystemAccessMode, - path: ServerNotification__FileSystemPath, +export const ServerNotification__ExternalAgentConfigImportTypeResult = Schema.Struct({ + failures: Schema.Array(ServerNotification__ExternalAgentConfigImportItemTypeFailure), + itemType: ServerNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array(ServerNotification__ExternalAgentConfigImportItemTypeSuccess), }); export type ServerNotification__FuzzyFileSearchSessionUpdatedNotification = { @@ -18513,6 +20212,28 @@ export const ServerNotification__HookRunSummary = Schema.Struct({ statusMessage: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ServerNotification__FileSystemPath = + | { readonly path: ServerNotification__LegacyAppPathString; readonly type: "path" } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { readonly type: "special"; readonly value: ServerNotification__FileSystemSpecialPath }; +export const ServerNotification__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: ServerNotification__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: ServerNotification__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); + export type ServerNotification__TurnError = { readonly additionalDetails?: string | null; readonly codexErrorInfo?: ServerNotification__CodexErrorInfo | null; @@ -18604,6 +20325,7 @@ export type ServerNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: ServerNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: ServerNotification__McpToolCallError | null; @@ -18650,13 +20372,15 @@ export type ServerNotification__ThreadItem = readonly action?: ServerNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: ServerNotification__AbsolutePathBuf; + readonly path: ServerNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -18719,10 +20443,7 @@ export const ServerNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -18770,6 +20491,9 @@ export const ServerNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([ServerNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -18782,7 +20506,14 @@ export const ServerNotification__ThreadItem = Schema.Union( ), error: Schema.optionalKey(Schema.Union([ServerNotification__McpToolCallError, Schema.Null])), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([ServerNotification__McpToolCallResult, Schema.Null]), @@ -18876,13 +20607,32 @@ export const ServerNotification__ThreadItem = Schema.Union( action: Schema.optionalKey(Schema.Union([ServerNotification__WebSearchAction, Schema.Null])), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: ServerNotification__AbsolutePathBuf, + path: ServerNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -18990,14 +20740,27 @@ export const ServerNotification__TurnPlanUpdatedNotification = Schema.Struct({ turnId: Schema.String, }); -export type ServerRequest__FileSystemSandboxEntry = { - readonly access: ServerRequest__FileSystemAccessMode; - readonly path: ServerRequest__FileSystemPath; -}; -export const ServerRequest__FileSystemSandboxEntry = Schema.Struct({ - access: ServerRequest__FileSystemAccessMode, - path: ServerRequest__FileSystemPath, -}); +export type ServerRequest__FileSystemPath = + | { readonly path: ServerRequest__LegacyAppPathString; readonly type: "path" } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { readonly type: "special"; readonly value: ServerRequest__FileSystemSpecialPath }; +export const ServerRequest__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: ServerRequest__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: ServerRequest__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); export type ServerRequest__McpElicitationTitledMultiSelectEnumSchema = { readonly default?: ReadonlyArray | null; @@ -19077,7 +20840,8 @@ export type ServerRequest__CommandExecutionRequestApprovalParams = { readonly approvalId?: string | null; readonly command?: string | null; readonly commandActions?: ReadonlyArray | null; - readonly cwd?: ServerRequest__AbsolutePathBuf | null; + readonly cwd?: ServerRequest__LegacyAppPathString | null; + readonly environmentId?: string | null; readonly itemId: string; readonly networkApprovalContext?: ServerRequest__NetworkApprovalContext | null; readonly proposedExecpolicyAmendment?: ReadonlyArray | null; @@ -19112,10 +20876,16 @@ export const ServerRequest__CommandExecutionRequestApprovalParams = Schema.Struc ]), ), cwd: Schema.optionalKey( - Schema.Union([ServerRequest__AbsolutePathBuf, Schema.Null]).annotate({ + Schema.Union([ServerRequest__LegacyAppPathString, Schema.Null]).annotate({ description: "The command's working directory.", }), ), + environmentId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Environment in which the command will run." }), + Schema.Null, + ]), + ), itemId: Schema.String, networkApprovalContext: Schema.optionalKey( Schema.Union([ServerRequest__NetworkApprovalContext, Schema.Null]).annotate({ @@ -19157,12 +20927,21 @@ export const ServerRequest__CommandExecutionRequestApprovalParams = Schema.Struc }); export type ServerRequest__ToolRequestUserInputParams = { + readonly autoResolutionMs?: number | null; readonly itemId: string; readonly questions: ReadonlyArray; readonly threadId: string; readonly turnId: string; }; export const ServerRequest__ToolRequestUserInputParams = Schema.Struct({ + autoResolutionMs: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null, + ]), + ), itemId: Schema.String, questions: Schema.Array(ServerRequest__ToolRequestUserInputQuestion), threadId: Schema.String, @@ -19174,6 +20953,8 @@ export type V2AppListUpdatedNotification__AppInfo = { readonly branding?: V2AppListUpdatedNotification__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -19193,6 +20974,12 @@ export const V2AppListUpdatedNotification__AppInfo = Schema.Struct({ ), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -19217,6 +21004,8 @@ export type V2AppsListResponse__AppInfo = { readonly branding?: V2AppsListResponse__AppBranding | null; readonly description?: string | null; readonly distributionChannel?: string | null; + readonly iconAssets?: { readonly [x: string]: string } | null; + readonly iconDarkAssets?: { readonly [x: string]: string } | null; readonly id: string; readonly installUrl?: string | null; readonly isAccessible?: boolean; @@ -19232,6 +21021,12 @@ export const V2AppsListResponse__AppInfo = Schema.Struct({ branding: Schema.optionalKey(Schema.Union([V2AppsListResponse__AppBranding, Schema.Null])), description: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), distributionChannel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + iconAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), + iconDarkAssets: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]), + ), id: Schema.String, installUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), isAccessible: Schema.optionalKey(Schema.Boolean.annotate({ default: false })), @@ -19282,6 +21077,15 @@ export const V2ConfigReadResponse__ToolsV2 = Schema.Struct({ ), }); +export type V2ConfigRequirementsReadResponse__ModelsRequirements = { + readonly newThread?: V2ConfigRequirementsReadResponse__NewThreadModelDefaults | null; +}; +export const V2ConfigRequirementsReadResponse__ModelsRequirements = Schema.Struct({ + newThread: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__NewThreadModelDefaults, Schema.Null]), + ), +}); + export type V2ConfigWriteResponse__ConfigLayerMetadata = { readonly name: V2ConfigWriteResponse__ConfigLayerSource; readonly version: string; @@ -19327,6 +21131,42 @@ export const V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationIt itemType: V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationItemType, }); +export type V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult = + { + readonly failures: ReadonlyArray; + readonly itemType: V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; + }; +export const V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult = + Schema.Struct({ + failures: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeFailure, + ), + itemType: + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory = { + readonly completedAtMs: number; + readonly failures: ReadonlyArray; + readonly importId: string; + readonly successes: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory = + Schema.Struct({ + completedAtMs: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + failures: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeFailure, + ), + importId: Schema.String, + successes: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + export type V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem = { readonly cwd?: string | null; readonly description: string; @@ -19350,6 +21190,39 @@ export const V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem itemType: V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItemType, }); +export type V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult = { + readonly failures: ReadonlyArray; + readonly itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType; + readonly successes: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult = + Schema.Struct({ + failures: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeFailure, + ), + itemType: V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigMigrationItemType, + successes: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportItemTypeSuccess, + ), + }); + +export type V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary = { + readonly availableCount: number; + readonly credits?: ReadonlyArray | null; +}; +export const V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary = Schema.Struct({ + availableCount: Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), + credits: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2GetAccountRateLimitsResponse__RateLimitResetCredit).annotate({ + description: + "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + }), + Schema.Null, + ]), + ), +}); + export type V2HookCompletedNotification__HookRunSummary = { readonly completedAt?: number | null; readonly displayOrder: number; @@ -19533,6 +21406,7 @@ export type V2ItemCompletedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ItemCompletedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ItemCompletedNotification__McpToolCallError | null; @@ -19581,13 +21455,15 @@ export type V2ItemCompletedNotification__ThreadItem = readonly action?: V2ItemCompletedNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ItemCompletedNotification__AbsolutePathBuf; + readonly path: V2ItemCompletedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -19652,10 +21528,7 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -19703,6 +21576,9 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ItemCompletedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -19717,7 +21593,14 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( Schema.Union([V2ItemCompletedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ItemCompletedNotification__McpToolCallResult, Schema.Null]), @@ -19814,13 +21697,32 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ItemCompletedNotification__AbsolutePathBuf, + path: V2ItemCompletedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -19855,25 +21757,61 @@ export const V2ItemCompletedNotification__ThreadItem = Schema.Union( { mode: "oneOf" }, ); -export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry = { - readonly access: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessMode; - readonly path: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath; -}; -export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry = - Schema.Struct({ - access: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessMode, - path: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath, - }); +export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = + | { + readonly path: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString; + readonly type: "path"; + } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath; + }; +export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); -export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry = { - readonly access: V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMode; - readonly path: V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath; -}; -export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry = - Schema.Struct({ - access: V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMode, - path: V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath, - }); +export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = + | { + readonly path: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString; + readonly type: "path"; + } + | { readonly pattern: string; readonly type: "glob_pattern" } + | { + readonly type: "special"; + readonly value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath; + }; +export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath = Schema.Union( + [ + Schema.Struct({ + path: V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString, + type: Schema.Literal("path").annotate({ title: "PathFileSystemPathType" }), + }).annotate({ title: "PathFileSystemPath" }), + Schema.Struct({ + pattern: Schema.String, + type: Schema.Literal("glob_pattern").annotate({ title: "GlobPatternFileSystemPathType" }), + }).annotate({ title: "GlobPatternFileSystemPath" }), + Schema.Struct({ + type: Schema.Literal("special").annotate({ title: "SpecialFileSystemPathType" }), + value: V2ItemGuardianApprovalReviewStartedNotification__FileSystemSpecialPath, + }).annotate({ title: "SpecialFileSystemPath" }), + ], + { mode: "oneOf" }, +); export type V2ItemStartedNotification__ThreadItem = | { @@ -19921,6 +21859,7 @@ export type V2ItemStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ItemStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ItemStartedNotification__McpToolCallError | null; @@ -19967,13 +21906,15 @@ export type V2ItemStartedNotification__ThreadItem = readonly action?: V2ItemStartedNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ItemStartedNotification__AbsolutePathBuf; + readonly path: V2ItemStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -20038,10 +21979,7 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -20089,6 +22027,9 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ItemStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -20103,7 +22044,14 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2ItemStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ItemStartedNotification__McpToolCallResult, Schema.Null]), @@ -20200,13 +22148,32 @@ export const V2ItemStartedNotification__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ItemStartedNotification__AbsolutePathBuf, + path: V2ItemStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -20405,6 +22372,19 @@ export const V2PluginReadResponse__PluginShareContext = Schema.Struct({ shareUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type V2PluginReadResponse__ScheduledTaskSummary = { + readonly key: string; + readonly name: string; + readonly prompt: string; + readonly schedule: V2PluginReadResponse__ScheduledTaskSchedule; +}; +export const V2PluginReadResponse__ScheduledTaskSummary = Schema.Struct({ + key: Schema.String, + name: Schema.String, + prompt: Schema.String, + schedule: V2PluginReadResponse__ScheduledTaskSchedule, +}); + export type V2PluginShareListResponse__PluginShareContext = { readonly creatorAccountUserId?: string | null; readonly creatorName?: string | null; @@ -20502,6 +22482,7 @@ export type V2ReviewStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ReviewStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ReviewStartResponse__McpToolCallError | null; @@ -20548,13 +22529,15 @@ export type V2ReviewStartResponse__ThreadItem = readonly action?: V2ReviewStartResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ReviewStartResponse__AbsolutePathBuf; + readonly path: V2ReviewStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -20617,10 +22600,7 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -20668,6 +22648,9 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ReviewStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -20682,7 +22665,14 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( Schema.Union([V2ReviewStartResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ReviewStartResponse__McpToolCallResult, Schema.Null]), @@ -20778,13 +22768,32 @@ export const V2ReviewStartResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ReviewStartResponse__AbsolutePathBuf, + path: V2ReviewStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -20909,6 +22918,7 @@ export type V2ThreadForkResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadForkResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadForkResponse__McpToolCallError | null; @@ -20955,13 +22965,15 @@ export type V2ThreadForkResponse__ThreadItem = readonly action?: V2ThreadForkResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadForkResponse__AbsolutePathBuf; + readonly path: V2ThreadForkResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21024,10 +23036,7 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21075,6 +23084,9 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadForkResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21089,7 +23101,14 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadForkResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadForkResponse__McpToolCallResult, Schema.Null]), @@ -21185,13 +23204,32 @@ export const V2ThreadForkResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadForkResponse__AbsolutePathBuf, + path: V2ThreadForkResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -21285,6 +23323,7 @@ export type V2ThreadListResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadListResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadListResponse__McpToolCallError | null; @@ -21331,13 +23370,15 @@ export type V2ThreadListResponse__ThreadItem = readonly action?: V2ThreadListResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadListResponse__AbsolutePathBuf; + readonly path: V2ThreadListResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21400,10 +23441,7 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21451,6 +23489,9 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadListResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21465,7 +23506,14 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadListResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadListResponse__McpToolCallResult, Schema.Null]), @@ -21561,13 +23609,32 @@ export const V2ThreadListResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadListResponse__AbsolutePathBuf, + path: V2ThreadListResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -21661,6 +23728,7 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadMetadataUpdateResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadMetadataUpdateResponse__McpToolCallError | null; @@ -21709,13 +23777,15 @@ export type V2ThreadMetadataUpdateResponse__ThreadItem = readonly action?: V2ThreadMetadataUpdateResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadMetadataUpdateResponse__AbsolutePathBuf; + readonly path: V2ThreadMetadataUpdateResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -21780,10 +23850,7 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -21831,6 +23898,9 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -21845,7 +23915,14 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadMetadataUpdateResponse__McpToolCallResult, Schema.Null]), @@ -21942,13 +24019,32 @@ export const V2ThreadMetadataUpdateResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadMetadataUpdateResponse__AbsolutePathBuf, + path: V2ThreadMetadataUpdateResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22042,6 +24138,7 @@ export type V2ThreadReadResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadReadResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadReadResponse__McpToolCallError | null; @@ -22088,13 +24185,15 @@ export type V2ThreadReadResponse__ThreadItem = readonly action?: V2ThreadReadResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadReadResponse__AbsolutePathBuf; + readonly path: V2ThreadReadResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22157,10 +24256,7 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22208,6 +24304,9 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadReadResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22222,7 +24321,14 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadReadResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadReadResponse__McpToolCallResult, Schema.Null]), @@ -22318,13 +24424,32 @@ export const V2ThreadReadResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadReadResponse__AbsolutePathBuf, + path: V2ThreadReadResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22426,6 +24551,7 @@ export type V2ThreadResumeResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadResumeResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadResumeResponse__McpToolCallError | null; @@ -22472,13 +24598,15 @@ export type V2ThreadResumeResponse__ThreadItem = readonly action?: V2ThreadResumeResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadResumeResponse__AbsolutePathBuf; + readonly path: V2ThreadResumeResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22541,10 +24669,7 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22592,6 +24717,9 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadResumeResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22606,7 +24734,14 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadResumeResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadResumeResponse__McpToolCallResult, Schema.Null]), @@ -22702,13 +24837,32 @@ export const V2ThreadResumeResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadResumeResponse__AbsolutePathBuf, + path: V2ThreadResumeResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -22802,6 +24956,7 @@ export type V2ThreadRollbackResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadRollbackResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadRollbackResponse__McpToolCallError | null; @@ -22848,13 +25003,15 @@ export type V2ThreadRollbackResponse__ThreadItem = readonly action?: V2ThreadRollbackResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadRollbackResponse__AbsolutePathBuf; + readonly path: V2ThreadRollbackResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -22919,10 +25076,7 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -22970,6 +25124,9 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadRollbackResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -22984,7 +25141,14 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadRollbackResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadRollbackResponse__McpToolCallResult, Schema.Null]), @@ -23081,13 +25245,32 @@ export const V2ThreadRollbackResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadRollbackResponse__AbsolutePathBuf, + path: V2ThreadRollbackResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23190,6 +25373,7 @@ export type V2ThreadStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadStartedNotification__McpToolCallError | null; @@ -23238,13 +25422,15 @@ export type V2ThreadStartedNotification__ThreadItem = readonly action?: V2ThreadStartedNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadStartedNotification__AbsolutePathBuf; + readonly path: V2ThreadStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -23309,10 +25495,7 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -23360,6 +25543,9 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -23374,7 +25560,14 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2ThreadStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadStartedNotification__McpToolCallResult, Schema.Null]), @@ -23471,13 +25664,32 @@ export const V2ThreadStartedNotification__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadStartedNotification__AbsolutePathBuf, + path: V2ThreadStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23571,6 +25783,7 @@ export type V2ThreadStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadStartResponse__McpToolCallError | null; @@ -23617,13 +25830,15 @@ export type V2ThreadStartResponse__ThreadItem = readonly action?: V2ThreadStartResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadStartResponse__AbsolutePathBuf; + readonly path: V2ThreadStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -23686,10 +25901,7 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -23737,6 +25949,9 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -23751,7 +25966,14 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadStartResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadStartResponse__McpToolCallResult, Schema.Null]), @@ -23847,13 +26069,32 @@ export const V2ThreadStartResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadStartResponse__AbsolutePathBuf, + path: V2ThreadStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -23947,6 +26188,7 @@ export type V2ThreadUnarchiveResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2ThreadUnarchiveResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2ThreadUnarchiveResponse__McpToolCallError | null; @@ -23993,13 +26235,15 @@ export type V2ThreadUnarchiveResponse__ThreadItem = readonly action?: V2ThreadUnarchiveResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2ThreadUnarchiveResponse__AbsolutePathBuf; + readonly path: V2ThreadUnarchiveResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24064,10 +26308,7 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24115,6 +26356,9 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2ThreadUnarchiveResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24129,7 +26373,14 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( Schema.Union([V2ThreadUnarchiveResponse__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2ThreadUnarchiveResponse__McpToolCallResult, Schema.Null]), @@ -24226,13 +26477,32 @@ export const V2ThreadUnarchiveResponse__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2ThreadUnarchiveResponse__AbsolutePathBuf, + path: V2ThreadUnarchiveResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -24326,6 +26596,7 @@ export type V2TurnCompletedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnCompletedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnCompletedNotification__McpToolCallError | null; @@ -24374,13 +26645,15 @@ export type V2TurnCompletedNotification__ThreadItem = readonly action?: V2TurnCompletedNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2TurnCompletedNotification__AbsolutePathBuf; + readonly path: V2TurnCompletedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24445,10 +26718,7 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24496,6 +26766,9 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnCompletedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24510,7 +26783,14 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( Schema.Union([V2TurnCompletedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnCompletedNotification__McpToolCallResult, Schema.Null]), @@ -24607,13 +26887,32 @@ export const V2TurnCompletedNotification__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnCompletedNotification__AbsolutePathBuf, + path: V2TurnCompletedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -24707,6 +27006,7 @@ export type V2TurnStartedNotification__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnStartedNotification__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnStartedNotification__McpToolCallError | null; @@ -24753,13 +27053,15 @@ export type V2TurnStartedNotification__ThreadItem = readonly action?: V2TurnStartedNotification__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2TurnStartedNotification__AbsolutePathBuf; + readonly path: V2TurnStartedNotification__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -24824,10 +27126,7 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -24875,6 +27174,9 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnStartedNotification__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -24889,7 +27191,14 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( Schema.Union([V2TurnStartedNotification__McpToolCallError, Schema.Null]), ), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnStartedNotification__McpToolCallResult, Schema.Null]), @@ -24986,13 +27295,32 @@ export const V2TurnStartedNotification__ThreadItem = Schema.Union( ), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnStartedNotification__AbsolutePathBuf, + path: V2TurnStartedNotification__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -25086,6 +27414,7 @@ export type V2TurnStartResponse__ThreadItem = readonly type: "fileChange"; } | { + readonly appContext?: V2TurnStartResponse__McpToolCallAppContext | null; readonly arguments: unknown; readonly durationMs?: number | null; readonly error?: V2TurnStartResponse__McpToolCallError | null; @@ -25132,13 +27461,15 @@ export type V2TurnStartResponse__ThreadItem = readonly action?: V2TurnStartResponse__WebSearchAction | null; readonly id: string; readonly query: string; + readonly results?: ReadonlyArray | null; readonly type: "webSearch"; } | { readonly id: string; - readonly path: V2TurnStartResponse__AbsolutePathBuf; + readonly path: V2TurnStartResponse__LegacyAppPathString; readonly type: "imageView"; } + | { readonly durationMs: number; readonly id: string; readonly type: "sleep" } | { readonly id: string; readonly result: string; @@ -25201,10 +27532,7 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( description: "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", }), - cwd: Schema.String.annotate({ - description: - "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", - }), + cwd: Schema.String.annotate({ description: "The command's working directory." }), durationMs: Schema.optionalKey( Schema.Union([ Schema.Number.annotate({ @@ -25252,6 +27580,9 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( type: Schema.Literal("fileChange").annotate({ title: "FileChangeThreadItemType" }), }).annotate({ title: "FileChangeThreadItem" }), Schema.Struct({ + appContext: Schema.optionalKey( + Schema.Union([V2TurnStartResponse__McpToolCallAppContext, Schema.Null]), + ), arguments: Schema.Unknown, durationMs: Schema.optionalKey( Schema.Union([ @@ -25264,7 +27595,14 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( ), error: Schema.optionalKey(Schema.Union([V2TurnStartResponse__McpToolCallError, Schema.Null])), id: Schema.String, - mcpAppResourceUri: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + mcpAppResourceUri: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Deprecated: use `appContext.resourceUri` instead.", + }), + Schema.Null, + ]), + ), pluginId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), result: Schema.optionalKey( Schema.Union([V2TurnStartResponse__McpToolCallResult, Schema.Null]), @@ -25358,13 +27696,32 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( action: Schema.optionalKey(Schema.Union([V2TurnStartResponse__WebSearchAction, Schema.Null])), id: Schema.String, query: Schema.String, + results: Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Unknown).annotate({ + description: + "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + }), + Schema.Null, + ]), + ), type: Schema.Literal("webSearch").annotate({ title: "WebSearchThreadItemType" }), }).annotate({ title: "WebSearchThreadItem" }), Schema.Struct({ id: Schema.String, - path: V2TurnStartResponse__AbsolutePathBuf, + path: V2TurnStartResponse__LegacyAppPathString, type: Schema.Literal("imageView").annotate({ title: "ImageViewThreadItemType" }), }).annotate({ title: "ImageViewThreadItem" }), + Schema.Struct({ + durationMs: Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + id: Schema.String, + type: Schema.Literal("sleep").annotate({ title: "SleepThreadItemType" }), + }).annotate({ + title: "SleepThreadItem", + description: "Display item emitted by the interruptible `clock.sleep` tool.", + }), Schema.Struct({ id: Schema.String, result: Schema.String, @@ -25401,51 +27758,38 @@ export const V2TurnStartResponse__ThreadItem = Schema.Union( export type ClientRequest__ExternalAgentConfigImportParams = { readonly migrationItems: ReadonlyArray; + readonly migrationSource?: string | null; + readonly source?: string | null; }; export const ClientRequest__ExternalAgentConfigImportParams = Schema.Struct({ migrationItems: Schema.Array(ClientRequest__ExternalAgentConfigMigrationItem), + migrationSource: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Migration-source selector used to produce the migration items. Pass the same value to detection and import; missing or unrecognized values use the default source.", + }), + Schema.Null, + ]), + ), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Optional identifier for the product that initiated the import.", + }), + Schema.Null, + ]), + ), }); -export type CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type CommandExecutionRequestApprovalParams__FileSystemSandboxEntry = { + readonly access: CommandExecutionRequestApprovalParams__FileSystemAccessMode; + readonly path: CommandExecutionRequestApprovalParams__FileSystemPath; }; -export const CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct( - { - entries: Schema.optionalKey( - Schema.Union([ - Schema.Array(CommandExecutionRequestApprovalParams__FileSystemSandboxEntry), - Schema.Null, - ]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(CommandExecutionRequestApprovalParams__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(CommandExecutionRequestApprovalParams__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - }, -); +export const CommandExecutionRequestApprovalParams__FileSystemSandboxEntry = Schema.Struct({ + access: CommandExecutionRequestApprovalParams__FileSystemAccessMode, + path: CommandExecutionRequestApprovalParams__FileSystemPath, +}); export type McpServerElicitationRequestParams__McpElicitationMultiSelectEnumSchema = | McpServerElicitationRequestParams__McpElicitationUntitledMultiSelectEnumSchema @@ -25455,82 +27799,22 @@ export const McpServerElicitationRequestParams__McpElicitationMultiSelectEnumSch McpServerElicitationRequestParams__McpElicitationTitledMultiSelectEnumSchema, ]); -export type PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type PermissionsRequestApprovalParams__FileSystemSandboxEntry = { + readonly access: PermissionsRequestApprovalParams__FileSystemAccessMode; + readonly path: PermissionsRequestApprovalParams__FileSystemPath; }; -export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalParams__FileSystemSandboxEntry), - Schema.Null, - ]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalParams__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalParams__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), +export const PermissionsRequestApprovalParams__FileSystemSandboxEntry = Schema.Struct({ + access: PermissionsRequestApprovalParams__FileSystemAccessMode, + path: PermissionsRequestApprovalParams__FileSystemPath, }); -export type PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type PermissionsRequestApprovalResponse__FileSystemSandboxEntry = { + readonly access: PermissionsRequestApprovalResponse__FileSystemAccessMode; + readonly path: PermissionsRequestApprovalResponse__FileSystemPath; }; -export const PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalResponse__FileSystemSandboxEntry), - Schema.Null, - ]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalResponse__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(PermissionsRequestApprovalResponse__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), +export const PermissionsRequestApprovalResponse__FileSystemSandboxEntry = Schema.Struct({ + access: PermissionsRequestApprovalResponse__FileSystemAccessMode, + path: PermissionsRequestApprovalResponse__FileSystemPath, }); export type ServerNotification__AppListUpdatedNotification = { @@ -25540,40 +27824,22 @@ export const ServerNotification__AppListUpdatedNotification = Schema.Struct({ data: Schema.Array(ServerNotification__AppInfo), }).annotate({ description: "EXPERIMENTAL - notification emitted when the app list changes." }); -export type ServerNotification__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type ServerNotification__ExternalAgentConfigImportCompletedNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; }; -export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([Schema.Array(ServerNotification__FileSystemSandboxEntry), Schema.Null]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(ServerNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(ServerNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), +export const ServerNotification__ExternalAgentConfigImportCompletedNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array(ServerNotification__ExternalAgentConfigImportTypeResult), +}); + +export type ServerNotification__ExternalAgentConfigImportProgressNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const ServerNotification__ExternalAgentConfigImportProgressNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array(ServerNotification__ExternalAgentConfigImportTypeResult), }); export type ServerNotification__HookCompletedNotification = { @@ -25598,6 +27864,15 @@ export const ServerNotification__HookStartedNotification = Schema.Struct({ turnId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }); +export type ServerNotification__FileSystemSandboxEntry = { + readonly access: ServerNotification__FileSystemAccessMode; + readonly path: ServerNotification__FileSystemPath; +}; +export const ServerNotification__FileSystemSandboxEntry = Schema.Struct({ + access: ServerNotification__FileSystemAccessMode, + path: ServerNotification__FileSystemPath, +}); + export type ServerNotification__ErrorNotification = { readonly error: ServerNotification__TurnError; readonly threadId: string; @@ -25708,7 +27983,9 @@ export const ServerNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(ServerNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -25730,40 +28007,13 @@ export const ServerNotification__Turn = Schema.Struct({ status: ServerNotification__TurnStatus, }); -export type ServerRequest__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type ServerRequest__FileSystemSandboxEntry = { + readonly access: ServerRequest__FileSystemAccessMode; + readonly path: ServerRequest__FileSystemPath; }; -export const ServerRequest__AdditionalFileSystemPermissions = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([Schema.Array(ServerRequest__FileSystemSandboxEntry), Schema.Null]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(ServerRequest__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(ServerRequest__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), +export const ServerRequest__FileSystemSandboxEntry = Schema.Struct({ + access: ServerRequest__FileSystemAccessMode, + path: ServerRequest__FileSystemPath, }); export type ServerRequest__McpElicitationMultiSelectEnumSchema = @@ -25868,6 +28118,58 @@ export const V2ConfigReadResponse__Config = Schema.StructWithRest( [Schema.Record(Schema.String, Schema.Unknown)], ); +export type V2ConfigRequirementsReadResponse__ConfigRequirements = { + readonly allowAppshots?: boolean | null; + readonly allowManagedHooksOnly?: boolean | null; + readonly allowRemoteControl?: boolean | null; + readonly allowedApprovalPolicies?: ReadonlyArray | null; + readonly allowedPermissionProfiles?: { readonly [x: string]: boolean } | null; + readonly allowedSandboxModes?: ReadonlyArray | null; + readonly allowedWebSearchModes?: ReadonlyArray | null; + readonly allowedWindowsSandboxImplementations?: ReadonlyArray | null; + readonly computerUse?: V2ConfigRequirementsReadResponse__ComputerUseRequirements | null; + readonly defaultPermissions?: string | null; + readonly enforceResidency?: V2ConfigRequirementsReadResponse__ResidencyRequirement | null; + readonly featureRequirements?: { readonly [x: string]: boolean } | null; + readonly models?: V2ConfigRequirementsReadResponse__ModelsRequirements | null; +}; +export const V2ConfigRequirementsReadResponse__ConfigRequirements = Schema.Struct({ + allowAppshots: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowManagedHooksOnly: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowRemoteControl: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + allowedApprovalPolicies: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__AskForApproval), Schema.Null]), + ), + allowedPermissionProfiles: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), + ), + allowedSandboxModes: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__SandboxMode), Schema.Null]), + ), + allowedWebSearchModes: Schema.optionalKey( + Schema.Union([Schema.Array(V2ConfigRequirementsReadResponse__WebSearchMode), Schema.Null]), + ), + allowedWindowsSandboxImplementations: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2ConfigRequirementsReadResponse__WindowsSandboxSetupMode), + Schema.Null, + ]), + ), + computerUse: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ComputerUseRequirements, Schema.Null]), + ), + defaultPermissions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + enforceResidency: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ResidencyRequirement, Schema.Null]), + ), + featureRequirements: Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Boolean), Schema.Null]), + ), + models: Schema.optionalKey( + Schema.Union([V2ConfigRequirementsReadResponse__ModelsRequirements, Schema.Null]), + ), +}); + export type V2ConfigWriteResponse__OverriddenMetadata = { readonly effectiveValue: unknown; readonly message: string; @@ -25879,84 +28181,24 @@ export const V2ConfigWriteResponse__OverriddenMetadata = Schema.Struct({ overridingLayer: V2ConfigWriteResponse__ConfigLayerMetadata, }); -export type V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry = { + readonly access: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessMode; + readonly path: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath; }; -export const V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = +export const V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry), - Schema.Null, - ]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), + access: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemAccessMode, + path: V2ItemGuardianApprovalReviewCompletedNotification__FileSystemPath, }); -export type V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = { - readonly entries?: ReadonlyArray | null; - readonly globScanMaxDepth?: number | null; - readonly read?: ReadonlyArray | null; - readonly write?: ReadonlyArray | null; +export type V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry = { + readonly access: V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMode; + readonly path: V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath; }; -export const V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = +export const V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry = Schema.Struct({ - entries: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry), - Schema.Null, - ]), - ), - globScanMaxDepth: Schema.optionalKey( - Schema.Union([ - Schema.Number.annotate({ format: "uint" }) - .check(Schema.isInt()) - .check(Schema.isGreaterThanOrEqualTo(1)), - Schema.Null, - ]), - ), - read: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), - write: Schema.optionalKey( - Schema.Union([ - Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__AbsolutePathBuf).annotate({ - description: "This will be removed in favor of `entries`.", - }), - Schema.Null, - ]), - ), + access: V2ItemGuardianApprovalReviewStartedNotification__FileSystemAccessMode, + path: V2ItemGuardianApprovalReviewStartedNotification__FileSystemPath, }); export type V2PluginInstalledResponse__PluginSummary = { @@ -25965,14 +28207,17 @@ export type V2PluginInstalledResponse__PluginSummary = { readonly enabled: boolean; readonly id: string; readonly installPolicy: V2PluginInstalledResponse__PluginInstallPolicy; + readonly installPolicySource?: V2PluginInstalledResponse__PluginInstallPolicySource | null; readonly installed: boolean; readonly interface?: V2PluginInstalledResponse__PluginInterface | null; readonly keywords?: ReadonlyArray; readonly localVersion?: string | null; + readonly mustShowInstallationInterstitial?: boolean | null; readonly name: string; readonly remotePluginId?: string | null; readonly shareContext?: V2PluginInstalledResponse__PluginShareContext | null; readonly source: V2PluginInstalledResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginInstalledResponse__PluginAuthPolicy, @@ -25985,6 +28230,9 @@ export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ enabled: Schema.Boolean, id: Schema.String, installPolicy: V2PluginInstalledResponse__PluginInstallPolicy, + installPolicySource: Schema.optionalKey( + Schema.Union([V2PluginInstalledResponse__PluginInstallPolicySource, Schema.Null]), + ), installed: Schema.Boolean, interface: Schema.optionalKey( Schema.Union([V2PluginInstalledResponse__PluginInterface, Schema.Null]), @@ -25998,6 +28246,7 @@ export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ Schema.Null, ]), ), + mustShowInstallationInterstitial: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), name: Schema.String, remotePluginId: Schema.optionalKey( Schema.Union([ @@ -26011,6 +28260,14 @@ export const V2PluginInstalledResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginInstalledResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginListResponse__PluginSummary = { @@ -26019,14 +28276,17 @@ export type V2PluginListResponse__PluginSummary = { readonly enabled: boolean; readonly id: string; readonly installPolicy: V2PluginListResponse__PluginInstallPolicy; + readonly installPolicySource?: V2PluginListResponse__PluginInstallPolicySource | null; readonly installed: boolean; readonly interface?: V2PluginListResponse__PluginInterface | null; readonly keywords?: ReadonlyArray; readonly localVersion?: string | null; + readonly mustShowInstallationInterstitial?: boolean | null; readonly name: string; readonly remotePluginId?: string | null; readonly shareContext?: V2PluginListResponse__PluginShareContext | null; readonly source: V2PluginListResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginListResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginListResponse__PluginAuthPolicy, @@ -26039,6 +28299,9 @@ export const V2PluginListResponse__PluginSummary = Schema.Struct({ enabled: Schema.Boolean, id: Schema.String, installPolicy: V2PluginListResponse__PluginInstallPolicy, + installPolicySource: Schema.optionalKey( + Schema.Union([V2PluginListResponse__PluginInstallPolicySource, Schema.Null]), + ), installed: Schema.Boolean, interface: Schema.optionalKey(Schema.Union([V2PluginListResponse__PluginInterface, Schema.Null])), keywords: Schema.optionalKey(Schema.Array(Schema.String).annotate({ default: [] })), @@ -26050,6 +28313,7 @@ export const V2PluginListResponse__PluginSummary = Schema.Struct({ Schema.Null, ]), ), + mustShowInstallationInterstitial: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), name: Schema.String, remotePluginId: Schema.optionalKey( Schema.Union([ @@ -26063,6 +28327,14 @@ export const V2PluginListResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginListResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginReadResponse__PluginSummary = { @@ -26071,14 +28343,17 @@ export type V2PluginReadResponse__PluginSummary = { readonly enabled: boolean; readonly id: string; readonly installPolicy: V2PluginReadResponse__PluginInstallPolicy; + readonly installPolicySource?: V2PluginReadResponse__PluginInstallPolicySource | null; readonly installed: boolean; readonly interface?: V2PluginReadResponse__PluginInterface | null; readonly keywords?: ReadonlyArray; readonly localVersion?: string | null; + readonly mustShowInstallationInterstitial?: boolean | null; readonly name: string; readonly remotePluginId?: string | null; readonly shareContext?: V2PluginReadResponse__PluginShareContext | null; readonly source: V2PluginReadResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginReadResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginReadResponse__PluginAuthPolicy, @@ -26091,6 +28366,9 @@ export const V2PluginReadResponse__PluginSummary = Schema.Struct({ enabled: Schema.Boolean, id: Schema.String, installPolicy: V2PluginReadResponse__PluginInstallPolicy, + installPolicySource: Schema.optionalKey( + Schema.Union([V2PluginReadResponse__PluginInstallPolicySource, Schema.Null]), + ), installed: Schema.Boolean, interface: Schema.optionalKey(Schema.Union([V2PluginReadResponse__PluginInterface, Schema.Null])), keywords: Schema.optionalKey(Schema.Array(Schema.String).annotate({ default: [] })), @@ -26102,6 +28380,7 @@ export const V2PluginReadResponse__PluginSummary = Schema.Struct({ Schema.Null, ]), ), + mustShowInstallationInterstitial: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), name: Schema.String, remotePluginId: Schema.optionalKey( Schema.Union([ @@ -26115,6 +28394,14 @@ export const V2PluginReadResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginReadResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2PluginShareListResponse__PluginSummary = { @@ -26123,14 +28410,17 @@ export type V2PluginShareListResponse__PluginSummary = { readonly enabled: boolean; readonly id: string; readonly installPolicy: V2PluginShareListResponse__PluginInstallPolicy; + readonly installPolicySource?: V2PluginShareListResponse__PluginInstallPolicySource | null; readonly installed: boolean; readonly interface?: V2PluginShareListResponse__PluginInterface | null; readonly keywords?: ReadonlyArray; readonly localVersion?: string | null; + readonly mustShowInstallationInterstitial?: boolean | null; readonly name: string; readonly remotePluginId?: string | null; readonly shareContext?: V2PluginShareListResponse__PluginShareContext | null; readonly source: V2PluginShareListResponse__PluginSource; + readonly version?: string | null; }; export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ authPolicy: V2PluginShareListResponse__PluginAuthPolicy, @@ -26143,6 +28433,9 @@ export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ enabled: Schema.Boolean, id: Schema.String, installPolicy: V2PluginShareListResponse__PluginInstallPolicy, + installPolicySource: Schema.optionalKey( + Schema.Union([V2PluginShareListResponse__PluginInstallPolicySource, Schema.Null]), + ), installed: Schema.Boolean, interface: Schema.optionalKey( Schema.Union([V2PluginShareListResponse__PluginInterface, Schema.Null]), @@ -26156,6 +28449,7 @@ export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ Schema.Null, ]), ), + mustShowInstallationInterstitial: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), name: Schema.String, remotePluginId: Schema.optionalKey( Schema.Union([ @@ -26169,12 +28463,21 @@ export const V2PluginShareListResponse__PluginSummary = Schema.Struct({ }), ), source: V2PluginShareListResponse__PluginSource, + version: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Version advertised by the remote marketplace backend when available.", + }), + Schema.Null, + ]), + ), }); export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly phase?: V2RawResponseItemCompletedNotification__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -26182,12 +28485,16 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -26195,6 +28502,7 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly action: V2RawResponseItemCompletedNotification__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status: V2RawResponseItemCompletedNotification__LocalShellStatus; readonly type: "local_shell_call"; } @@ -26202,6 +28510,7 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -26211,11 +28520,14 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -26223,12 +28535,16 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -26236,6 +28552,8 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -26243,26 +28561,42 @@ export type V2RawResponseItemCompletedNotification__ResponseItem = | { readonly action?: V2RawResponseItemCompletedNotification__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(V2RawResponseItemCompletedNotification__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), phase: Schema.optionalKey( Schema.Union([V2RawResponseItemCompletedNotification__MessagePhase, Schema.Null]), @@ -26273,6 +28607,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ author: Schema.String, content: Schema.Array(V2RawResponseItemCompletedNotification__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -26284,6 +28625,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union ]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), summary: Schema.Array(V2RawResponseItemCompletedNotification__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -26299,11 +28647,16 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), status: V2RawResponseItemCompletedNotification__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -26312,8 +28665,12 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -26323,8 +28680,12 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -26333,6 +28694,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -26340,11 +28708,16 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -26352,6 +28725,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: V2RawResponseItemCompletedNotification__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -26361,6 +28741,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -26374,14 +28761,24 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union Schema.Null, ]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -26391,6 +28788,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -26400,6 +28804,13 @@ export const V2RawResponseItemCompletedNotification__ResponseItem = Schema.Union }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ + V2RawResponseItemCompletedNotification__InternalChatMessageMetadataPassthrough, + Schema.Null, + ]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -26445,7 +28856,9 @@ export const V2ReviewStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ReviewStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26512,7 +28925,9 @@ export const V2ThreadForkResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadForkResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26568,7 +28983,9 @@ export const V2ThreadListResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadListResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26624,7 +29041,9 @@ export const V2ThreadMetadataUpdateResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadMetadataUpdateResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26680,7 +29099,9 @@ export const V2ThreadReadResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadReadResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26736,7 +29157,9 @@ export const V2ThreadResumeResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadResumeResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26792,7 +29215,9 @@ export const V2ThreadRollbackResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadRollbackResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26885,7 +29310,9 @@ export const V2ThreadStartedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadStartedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26941,7 +29368,9 @@ export const V2ThreadStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -26997,7 +29426,9 @@ export const V2ThreadUnarchiveResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2ThreadUnarchiveResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27053,7 +29484,9 @@ export const V2TurnCompletedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnCompletedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27109,7 +29542,9 @@ export const V2TurnStartedNotification__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnStartedNotification__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27165,7 +29600,9 @@ export const V2TurnStartResponse__Turn = Schema.Struct({ description: "Only populated when the Turn's status is failed.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + }), items: Schema.Array(V2TurnStartResponse__ThreadItem).annotate({ description: "Thread items currently included in this turn payload.", }), @@ -27187,6 +29624,47 @@ export const V2TurnStartResponse__Turn = Schema.Struct({ status: V2TurnStartResponse__TurnStatus, }); +export type CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; +}; +export const CommandExecutionRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct( + { + entries: Schema.optionalKey( + Schema.Union([ + Schema.Array(CommandExecutionRequestApprovalParams__FileSystemSandboxEntry), + Schema.Null, + ]), + ), + globScanMaxDepth: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(CommandExecutionRequestApprovalParams__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(CommandExecutionRequestApprovalParams__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + }, +); + export type McpServerElicitationRequestParams__McpElicitationEnumSchema = | McpServerElicitationRequestParams__McpElicitationSingleSelectEnumSchema | McpServerElicitationRequestParams__McpElicitationMultiSelectEnumSchema @@ -27197,45 +29675,117 @@ export const McpServerElicitationRequestParams__McpElicitationEnumSchema = Schem McpServerElicitationRequestParams__McpElicitationLegacyTitledEnumSchema, ]); -export type PermissionsRequestApprovalParams__RequestPermissionProfile = { - readonly fileSystem?: PermissionsRequestApprovalParams__AdditionalFileSystemPermissions | null; - readonly network?: PermissionsRequestApprovalParams__AdditionalNetworkPermissions | null; +export type PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const PermissionsRequestApprovalParams__RequestPermissionProfile = Schema.Struct({ - fileSystem: Schema.optionalKey( - Schema.Union([PermissionsRequestApprovalParams__AdditionalFileSystemPermissions, Schema.Null]), +export const PermissionsRequestApprovalParams__AdditionalFileSystemPermissions = Schema.Struct({ + entries: Schema.optionalKey( + Schema.Union([ + Schema.Array(PermissionsRequestApprovalParams__FileSystemSandboxEntry), + Schema.Null, + ]), ), - network: Schema.optionalKey( - Schema.Union([PermissionsRequestApprovalParams__AdditionalNetworkPermissions, Schema.Null]), + globScanMaxDepth: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(PermissionsRequestApprovalParams__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(PermissionsRequestApprovalParams__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), ), }); -export type PermissionsRequestApprovalResponse__GrantedPermissionProfile = { - readonly fileSystem?: PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions | null; - readonly network?: PermissionsRequestApprovalResponse__AdditionalNetworkPermissions | null; +export type PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const PermissionsRequestApprovalResponse__GrantedPermissionProfile = Schema.Struct({ - fileSystem: Schema.optionalKey( +export const PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions = Schema.Struct({ + entries: Schema.optionalKey( Schema.Union([ - PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions, + Schema.Array(PermissionsRequestApprovalResponse__FileSystemSandboxEntry), Schema.Null, ]), ), - network: Schema.optionalKey( - Schema.Union([PermissionsRequestApprovalResponse__AdditionalNetworkPermissions, Schema.Null]), + globScanMaxDepth: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(PermissionsRequestApprovalResponse__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(PermissionsRequestApprovalResponse__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), ), }); -export type ServerNotification__RequestPermissionProfile = { - readonly fileSystem?: ServerNotification__AdditionalFileSystemPermissions | null; - readonly network?: ServerNotification__AdditionalNetworkPermissions | null; +export type ServerNotification__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const ServerNotification__RequestPermissionProfile = Schema.Struct({ - fileSystem: Schema.optionalKey( - Schema.Union([ServerNotification__AdditionalFileSystemPermissions, Schema.Null]), +export const ServerNotification__AdditionalFileSystemPermissions = Schema.Struct({ + entries: Schema.optionalKey( + Schema.Union([Schema.Array(ServerNotification__FileSystemSandboxEntry), Schema.Null]), ), - network: Schema.optionalKey( - Schema.Union([ServerNotification__AdditionalNetworkPermissions, Schema.Null]), + globScanMaxDepth: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(ServerNotification__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(ServerNotification__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), ), }); @@ -27263,6 +29813,7 @@ export type ServerNotification__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27329,7 +29880,9 @@ export const ServerNotification__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27357,6 +29910,15 @@ export const ServerNotification__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27423,16 +29985,39 @@ export const ServerNotification__TurnStartedNotification = Schema.Struct({ turn: ServerNotification__Turn, }); -export type ServerRequest__RequestPermissionProfile = { - readonly fileSystem?: ServerRequest__AdditionalFileSystemPermissions | null; - readonly network?: ServerRequest__AdditionalNetworkPermissions | null; +export type ServerRequest__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const ServerRequest__RequestPermissionProfile = Schema.Struct({ - fileSystem: Schema.optionalKey( - Schema.Union([ServerRequest__AdditionalFileSystemPermissions, Schema.Null]), +export const ServerRequest__AdditionalFileSystemPermissions = Schema.Struct({ + entries: Schema.optionalKey( + Schema.Union([Schema.Array(ServerRequest__FileSystemSandboxEntry), Schema.Null]), ), - network: Schema.optionalKey( - Schema.Union([ServerRequest__AdditionalNetworkPermissions, Schema.Null]), + globScanMaxDepth: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(ServerRequest__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(ServerRequest__LegacyAppPathString).annotate({ + description: "This will be removed in favor of `entries`.", + }), + Schema.Null, + ]), ), }); @@ -27446,41 +30031,81 @@ export const ServerRequest__McpElicitationEnumSchema = Schema.Union([ ServerRequest__McpElicitationLegacyTitledEnumSchema, ]); -export type V2ItemGuardianApprovalReviewCompletedNotification__RequestPermissionProfile = { - readonly fileSystem?: V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions | null; - readonly network?: V2ItemGuardianApprovalReviewCompletedNotification__AdditionalNetworkPermissions | null; +export type V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const V2ItemGuardianApprovalReviewCompletedNotification__RequestPermissionProfile = +export const V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions = Schema.Struct({ - fileSystem: Schema.optionalKey( + entries: Schema.optionalKey( Schema.Union([ - V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions, + Schema.Array(V2ItemGuardianApprovalReviewCompletedNotification__FileSystemSandboxEntry), Schema.Null, ]), ), - network: Schema.optionalKey( + globScanMaxDepth: Schema.optionalKey( Schema.Union([ - V2ItemGuardianApprovalReviewCompletedNotification__AdditionalNetworkPermissions, + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array( + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + ).annotate({ description: "This will be removed in favor of `entries`." }), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array( + V2ItemGuardianApprovalReviewCompletedNotification__LegacyAppPathString, + ).annotate({ description: "This will be removed in favor of `entries`." }), Schema.Null, ]), ), }); -export type V2ItemGuardianApprovalReviewStartedNotification__RequestPermissionProfile = { - readonly fileSystem?: V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions | null; - readonly network?: V2ItemGuardianApprovalReviewStartedNotification__AdditionalNetworkPermissions | null; +export type V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = { + readonly entries?: ReadonlyArray | null; + readonly globScanMaxDepth?: number | null; + readonly read?: ReadonlyArray | null; + readonly write?: ReadonlyArray | null; }; -export const V2ItemGuardianApprovalReviewStartedNotification__RequestPermissionProfile = +export const V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions = Schema.Struct({ - fileSystem: Schema.optionalKey( + entries: Schema.optionalKey( Schema.Union([ - V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions, + Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__FileSystemSandboxEntry), Schema.Null, ]), ), - network: Schema.optionalKey( + globScanMaxDepth: Schema.optionalKey( Schema.Union([ - V2ItemGuardianApprovalReviewStartedNotification__AdditionalNetworkPermissions, + Schema.Number.annotate({ format: "uint" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(1)), + Schema.Null, + ]), + ), + read: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString).annotate( + { description: "This will be removed in favor of `entries`." }, + ), + Schema.Null, + ]), + ), + write: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2ItemGuardianApprovalReviewStartedNotification__LegacyAppPathString).annotate( + { description: "This will be removed in favor of `entries`." }, + ), Schema.Null, ]), ), @@ -27534,6 +30159,8 @@ export type V2PluginReadResponse__PluginDetail = { readonly marketplaceName: string; readonly marketplacePath?: V2PluginReadResponse__AbsolutePathBuf | null; readonly mcpServers: ReadonlyArray; + readonly scheduledTasks?: ReadonlyArray | null; + readonly shareUrl?: string | null; readonly skills: ReadonlyArray; readonly summary: V2PluginReadResponse__PluginSummary; }; @@ -27547,6 +30174,10 @@ export const V2PluginReadResponse__PluginDetail = Schema.Struct({ Schema.Union([V2PluginReadResponse__AbsolutePathBuf, Schema.Null]), ), mcpServers: Schema.Array(Schema.String), + scheduledTasks: Schema.optionalKey( + Schema.Union([Schema.Array(V2PluginReadResponse__ScheduledTaskSummary), Schema.Null]), + ), + shareUrl: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), skills: Schema.Array(V2PluginReadResponse__SkillSummary), summary: V2PluginReadResponse__PluginSummary, }); @@ -27577,6 +30208,7 @@ export type V2ThreadForkResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27643,7 +30275,9 @@ export const V2ThreadForkResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27671,6 +30305,15 @@ export const V2ThreadForkResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27734,6 +30377,7 @@ export type V2ThreadListResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27800,7 +30444,9 @@ export const V2ThreadListResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27828,6 +30474,15 @@ export const V2ThreadListResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -27891,6 +30546,7 @@ export type V2ThreadMetadataUpdateResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -27957,7 +30613,9 @@ export const V2ThreadMetadataUpdateResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -27985,6 +30643,15 @@ export const V2ThreadMetadataUpdateResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28048,6 +30715,7 @@ export type V2ThreadReadResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28114,7 +30782,9 @@ export const V2ThreadReadResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28142,6 +30812,15 @@ export const V2ThreadReadResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28205,6 +30884,7 @@ export type V2ThreadResumeResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28271,7 +30951,9 @@ export const V2ThreadResumeResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28299,6 +30981,15 @@ export const V2ThreadResumeResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28362,6 +31053,7 @@ export type V2ThreadStartedNotification__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28428,7 +31120,9 @@ export const V2ThreadStartedNotification__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28456,6 +31150,15 @@ export const V2ThreadStartedNotification__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28519,6 +31222,7 @@ export type V2ThreadStartResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28585,7 +31289,9 @@ export const V2ThreadStartResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28613,6 +31319,15 @@ export const V2ThreadStartResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28676,6 +31391,7 @@ export type V2ThreadUnarchiveResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -28742,7 +31458,9 @@ export const V2ThreadUnarchiveResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -28770,6 +31488,15 @@ export const V2ThreadUnarchiveResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -28830,6 +31557,141 @@ export const McpServerElicitationRequestParams__McpElicitationPrimitiveSchema = McpServerElicitationRequestParams__McpElicitationBooleanSchema, ]); +export type PermissionsRequestApprovalParams__RequestPermissionProfile = { + readonly fileSystem?: PermissionsRequestApprovalParams__AdditionalFileSystemPermissions | null; + readonly network?: PermissionsRequestApprovalParams__AdditionalNetworkPermissions | null; +}; +export const PermissionsRequestApprovalParams__RequestPermissionProfile = Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalParams__AdditionalFileSystemPermissions, Schema.Null]), + ), + network: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalParams__AdditionalNetworkPermissions, Schema.Null]), + ), +}); + +export type PermissionsRequestApprovalResponse__GrantedPermissionProfile = { + readonly fileSystem?: PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions | null; + readonly network?: PermissionsRequestApprovalResponse__AdditionalNetworkPermissions | null; +}; +export const PermissionsRequestApprovalResponse__GrantedPermissionProfile = Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([ + PermissionsRequestApprovalResponse__AdditionalFileSystemPermissions, + Schema.Null, + ]), + ), + network: Schema.optionalKey( + Schema.Union([PermissionsRequestApprovalResponse__AdditionalNetworkPermissions, Schema.Null]), + ), +}); + +export type ServerNotification__RequestPermissionProfile = { + readonly fileSystem?: ServerNotification__AdditionalFileSystemPermissions | null; + readonly network?: ServerNotification__AdditionalNetworkPermissions | null; +}; +export const ServerNotification__RequestPermissionProfile = Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([ServerNotification__AdditionalFileSystemPermissions, Schema.Null]), + ), + network: Schema.optionalKey( + Schema.Union([ServerNotification__AdditionalNetworkPermissions, Schema.Null]), + ), +}); + +export type ServerNotification__ThreadStartedNotification = { + readonly thread: ServerNotification__Thread; +}; +export const ServerNotification__ThreadStartedNotification = Schema.Struct({ + thread: ServerNotification__Thread, +}); + +export type ServerRequest__RequestPermissionProfile = { + readonly fileSystem?: ServerRequest__AdditionalFileSystemPermissions | null; + readonly network?: ServerRequest__AdditionalNetworkPermissions | null; +}; +export const ServerRequest__RequestPermissionProfile = Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([ServerRequest__AdditionalFileSystemPermissions, Schema.Null]), + ), + network: Schema.optionalKey( + Schema.Union([ServerRequest__AdditionalNetworkPermissions, Schema.Null]), + ), +}); + +export type ServerRequest__McpElicitationPrimitiveSchema = + | ServerRequest__McpElicitationEnumSchema + | ServerRequest__McpElicitationStringSchema + | ServerRequest__McpElicitationNumberSchema + | ServerRequest__McpElicitationBooleanSchema; +export const ServerRequest__McpElicitationPrimitiveSchema = Schema.Union([ + ServerRequest__McpElicitationEnumSchema, + ServerRequest__McpElicitationStringSchema, + ServerRequest__McpElicitationNumberSchema, + ServerRequest__McpElicitationBooleanSchema, +]); + +export type V2ItemGuardianApprovalReviewCompletedNotification__RequestPermissionProfile = { + readonly fileSystem?: V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions | null; + readonly network?: V2ItemGuardianApprovalReviewCompletedNotification__AdditionalNetworkPermissions | null; +}; +export const V2ItemGuardianApprovalReviewCompletedNotification__RequestPermissionProfile = + Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewCompletedNotification__AdditionalFileSystemPermissions, + Schema.Null, + ]), + ), + network: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewCompletedNotification__AdditionalNetworkPermissions, + Schema.Null, + ]), + ), + }); + +export type V2ItemGuardianApprovalReviewStartedNotification__RequestPermissionProfile = { + readonly fileSystem?: V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions | null; + readonly network?: V2ItemGuardianApprovalReviewStartedNotification__AdditionalNetworkPermissions | null; +}; +export const V2ItemGuardianApprovalReviewStartedNotification__RequestPermissionProfile = + Schema.Struct({ + fileSystem: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewStartedNotification__AdditionalFileSystemPermissions, + Schema.Null, + ]), + ), + network: Schema.optionalKey( + Schema.Union([ + V2ItemGuardianApprovalReviewStartedNotification__AdditionalNetworkPermissions, + Schema.Null, + ]), + ), + }); + +export type McpServerElicitationRequestParams__McpElicitationSchema = { + readonly $schema?: string | null; + readonly properties: { + readonly [x: string]: McpServerElicitationRequestParams__McpElicitationPrimitiveSchema; + }; + readonly required?: ReadonlyArray | null; + readonly type: McpServerElicitationRequestParams__McpElicitationObjectType; +}; +export const McpServerElicitationRequestParams__McpElicitationSchema = Schema.Struct({ + $schema: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + properties: Schema.Record( + Schema.String, + McpServerElicitationRequestParams__McpElicitationPrimitiveSchema, + ), + required: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + type: McpServerElicitationRequestParams__McpElicitationObjectType, +}).annotate({ + description: + "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", +}); + export type ServerNotification__GuardianApprovalReviewAction = | { readonly command: string; @@ -28925,13 +31787,6 @@ export const ServerNotification__GuardianApprovalReviewAction = Schema.Union( { mode: "oneOf" }, ); -export type ServerNotification__ThreadStartedNotification = { - readonly thread: ServerNotification__Thread; -}; -export const ServerNotification__ThreadStartedNotification = Schema.Struct({ - thread: ServerNotification__Thread, -}); - export type ServerRequest__PermissionsRequestApprovalParams = { readonly cwd: ServerRequest__AbsolutePathBuf; readonly environmentId?: string | null; @@ -28956,17 +31811,21 @@ export const ServerRequest__PermissionsRequestApprovalParams = Schema.Struct({ turnId: Schema.String, }); -export type ServerRequest__McpElicitationPrimitiveSchema = - | ServerRequest__McpElicitationEnumSchema - | ServerRequest__McpElicitationStringSchema - | ServerRequest__McpElicitationNumberSchema - | ServerRequest__McpElicitationBooleanSchema; -export const ServerRequest__McpElicitationPrimitiveSchema = Schema.Union([ - ServerRequest__McpElicitationEnumSchema, - ServerRequest__McpElicitationStringSchema, - ServerRequest__McpElicitationNumberSchema, - ServerRequest__McpElicitationBooleanSchema, -]); +export type ServerRequest__McpElicitationSchema = { + readonly $schema?: string | null; + readonly properties: { readonly [x: string]: ServerRequest__McpElicitationPrimitiveSchema }; + readonly required?: ReadonlyArray | null; + readonly type: ServerRequest__McpElicitationObjectType; +}; +export const ServerRequest__McpElicitationSchema = Schema.Struct({ + $schema: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + properties: Schema.Record(Schema.String, ServerRequest__McpElicitationPrimitiveSchema), + required: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + type: ServerRequest__McpElicitationObjectType, +}).annotate({ + description: + "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", +}); export type V2ItemGuardianApprovalReviewCompletedNotification__GuardianApprovalReviewAction = | { @@ -29164,27 +32023,6 @@ export const V2ItemGuardianApprovalReviewStartedNotification__GuardianApprovalRe { mode: "oneOf" }, ); -export type McpServerElicitationRequestParams__McpElicitationSchema = { - readonly $schema?: string | null; - readonly properties: { - readonly [x: string]: McpServerElicitationRequestParams__McpElicitationPrimitiveSchema; - }; - readonly required?: ReadonlyArray | null; - readonly type: McpServerElicitationRequestParams__McpElicitationObjectType; -}; -export const McpServerElicitationRequestParams__McpElicitationSchema = Schema.Struct({ - $schema: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - properties: Schema.Record( - Schema.String, - McpServerElicitationRequestParams__McpElicitationPrimitiveSchema, - ), - required: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), - type: McpServerElicitationRequestParams__McpElicitationObjectType, -}).annotate({ - description: - "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", -}); - export type ServerNotification__ItemGuardianApprovalReviewCompletedNotification = { readonly action: ServerNotification__GuardianApprovalReviewAction; readonly completedAtMs: number; @@ -29258,22 +32096,6 @@ export const ServerNotification__ItemGuardianApprovalReviewStartedNotification = "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.", }); -export type ServerRequest__McpElicitationSchema = { - readonly $schema?: string | null; - readonly properties: { readonly [x: string]: ServerRequest__McpElicitationPrimitiveSchema }; - readonly required?: ReadonlyArray | null; - readonly type: ServerRequest__McpElicitationObjectType; -}; -export const ServerRequest__McpElicitationSchema = Schema.Struct({ - $schema: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - properties: Schema.Record(Schema.String, ServerRequest__McpElicitationPrimitiveSchema), - required: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), - type: ServerRequest__McpElicitationObjectType, -}).annotate({ - description: - "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", -}); - export type ServerRequest__McpServerElicitationRequestParams = | { readonly _meta?: unknown; @@ -29284,6 +32106,15 @@ export type ServerRequest__McpServerElicitationRequestParams = readonly threadId: string; readonly turnId?: string | null; } + | { + readonly _meta?: unknown; + readonly message: string; + readonly mode: "openai/form"; + readonly requestedSchema: unknown; + readonly serverName: string; + readonly threadId: string; + readonly turnId?: string | null; + } | { readonly _meta?: unknown; readonly elicitationId: string; @@ -29313,6 +32144,23 @@ export const ServerRequest__McpServerElicitationRequestParams = Schema.Union( ]), ), }), + Schema.Struct({ + _meta: Schema.optionalKey(Schema.Unknown), + message: Schema.String, + mode: Schema.Literal("openai/form"), + requestedSchema: Schema.Unknown, + serverName: Schema.String, + threadId: Schema.String, + turnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + }), + Schema.Null, + ]), + ), + }), Schema.Struct({ _meta: Schema.optionalKey(Schema.Unknown), elicitationId: Schema.String, @@ -29604,11 +32452,21 @@ export type ClientRequest = readonly method: "plugin/share/delete"; readonly params: ClientRequest__PluginShareDeleteParams; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "app/read"; + readonly params: ClientRequest__AppsReadParams; + } | { readonly id: ClientRequest__RequestId; readonly method: "app/list"; readonly params: ClientRequest__AppsListParams; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "app/installed"; + readonly params: ClientRequest__AppsInstalledParams; + } | { readonly id: ClientRequest__RequestId; readonly method: "fs/readFile"; @@ -29769,11 +32627,21 @@ export type ClientRequest = readonly method: "account/rateLimits/read"; readonly params?: null; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "account/rateLimitResetCredit/consume"; + readonly params: ClientRequest__ConsumeAccountRateLimitResetCreditParams; + } | { readonly id: ClientRequest__RequestId; readonly method: "account/usage/read"; readonly params?: null; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "account/workspaceMessages/read"; + readonly params?: null; + } | { readonly id: ClientRequest__RequestId; readonly method: "account/sendAddCreditsNudgeEmail"; @@ -29819,6 +32687,11 @@ export type ClientRequest = readonly method: "externalAgentConfig/import"; readonly params: ClientRequest__ExternalAgentConfigImportParams; } + | { + readonly id: ClientRequest__RequestId; + readonly method: "externalAgentConfig/import/readHistories"; + readonly params?: null; + } | { readonly id: ClientRequest__RequestId; readonly method: "config/value/write"; @@ -30068,11 +32941,21 @@ export const ClientRequest = Schema.Union( }), params: ClientRequest__PluginShareDeleteParams, }).annotate({ title: "Plugin/share/deleteRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("app/read").annotate({ title: "App/readRequestMethod" }), + params: ClientRequest__AppsReadParams, + }).annotate({ title: "App/readRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("app/list").annotate({ title: "App/listRequestMethod" }), params: ClientRequest__AppsListParams, }).annotate({ title: "App/listRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("app/installed").annotate({ title: "App/installedRequestMethod" }), + params: ClientRequest__AppsInstalledParams, + }).annotate({ title: "App/installedRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("fs/readFile").annotate({ title: "Fs/readFileRequestMethod" }), @@ -30269,6 +33152,13 @@ export const ClientRequest = Schema.Union( }), params: Schema.optionalKey(Schema.Null), }).annotate({ title: "Account/rateLimits/readRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("account/rateLimitResetCredit/consume").annotate({ + title: "Account/rateLimitResetCredit/consumeRequestMethod", + }), + params: ClientRequest__ConsumeAccountRateLimitResetCreditParams, + }).annotate({ title: "Account/rateLimitResetCredit/consumeRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("account/usage/read").annotate({ @@ -30276,6 +33166,13 @@ export const ClientRequest = Schema.Union( }), params: Schema.optionalKey(Schema.Null), }).annotate({ title: "Account/usage/readRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("account/workspaceMessages/read").annotate({ + title: "Account/workspaceMessages/readRequestMethod", + }), + params: Schema.optionalKey(Schema.Null), + }).annotate({ title: "Account/workspaceMessages/readRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("account/sendAddCreditsNudgeEmail").annotate({ @@ -30346,6 +33243,13 @@ export const ClientRequest = Schema.Union( }), params: ClientRequest__ExternalAgentConfigImportParams, }).annotate({ title: "ExternalAgentConfig/importRequest" }), + Schema.Struct({ + id: ClientRequest__RequestId, + method: Schema.Literal("externalAgentConfig/import/readHistories").annotate({ + title: "ExternalAgentConfig/import/readHistoriesRequestMethod", + }), + params: Schema.optionalKey(Schema.Null), + }).annotate({ title: "ExternalAgentConfig/import/readHistoriesRequest" }), Schema.Struct({ id: ClientRequest__RequestId, method: Schema.Literal("config/value/write").annotate({ @@ -30409,7 +33313,9 @@ export const ClientRequest__CapabilityRootLocation = Schema.Union( [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -30421,6 +33327,13 @@ export const ClientRequest__CapabilityRootLocation = Schema.Union( { mode: "oneOf" }, ).annotate({ description: "Location used to resolve a selected capability root." }); +export type ClientRequest__CodexResponseHandoffMode = "thinking" | "commentary" | "bemTags"; +export const ClientRequest__CodexResponseHandoffMode = Schema.Literals([ + "thinking", + "commentary", + "bemTags", +]); + export type ClientRequest__CollaborationMode = { readonly mode: ClientRequest__ModeKind; readonly settings: ClientRequest__Settings; @@ -30430,19 +33343,52 @@ export const ClientRequest__CollaborationMode = Schema.Struct({ settings: ClientRequest__Settings, }).annotate({ description: "Collaboration mode for a Codex session." }); -export type ClientRequest__DynamicToolSpec = { - readonly deferLoading?: boolean; - readonly description: string; - readonly inputSchema: unknown; - readonly name: string; - readonly namespace?: string | null; -}; -export const ClientRequest__DynamicToolSpec = Schema.Struct({ - deferLoading: Schema.optionalKey(Schema.Boolean), - description: Schema.String, - inputSchema: Schema.Unknown, - name: Schema.String, - namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +export type ClientRequest__DynamicToolSpec = + | { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; + } + | { + readonly description: string; + readonly name: string; + readonly tools: ReadonlyArray; + readonly type: "namespace"; + }; +export const ClientRequest__DynamicToolSpec = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolSpecType" }), + }).annotate({ title: "FunctionDynamicToolSpec" }), + Schema.Struct({ + description: Schema.String, + name: Schema.String, + tools: Schema.Array(ClientRequest__DynamicToolNamespaceTool), + type: Schema.Literal("namespace").annotate({ title: "NamespaceDynamicToolSpecType" }), + }).annotate({ title: "NamespaceDynamicToolSpec" }), + ], + { mode: "oneOf" }, +); + +export type ClientRequest__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const ClientRequest__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", }); export type ClientRequest__NetworkAccess = "restricted" | "enabled"; @@ -30464,8 +33410,8 @@ export const ClientRequest__ProcessTerminalSize = Schema.Struct({ .check(Schema.isGreaterThanOrEqualTo(0)), }).annotate({ description: "PTY size in character cells for `process/spawn` PTY sessions." }); -export type ClientRequest__RealtimeConversationVersion = "v1" | "v2"; -export const ClientRequest__RealtimeConversationVersion = Schema.Literals(["v1", "v2"]); +export type ClientRequest__RealtimeConversationVersion = "v1" | "v2" | "v3"; +export const ClientRequest__RealtimeConversationVersion = Schema.Literals(["v1", "v2", "v3"]); export type ClientRequest__RealtimeOutputModality = "text" | "audio"; export const ClientRequest__RealtimeOutputModality = Schema.Literals(["text", "audio"]); @@ -30512,10 +33458,21 @@ export const ClientRequest__RealtimeVoice = Schema.Literals([ "verse", ]); +export type ClientRequest__RemoteControlDisableParams = { readonly ephemeral?: boolean }; +export const ClientRequest__RemoteControlDisableParams = Schema.Struct({ + ephemeral: Schema.optionalKey(Schema.Boolean), +}); + +export type ClientRequest__RemoteControlEnableParams = { readonly ephemeral?: boolean }; +export const ClientRequest__RemoteControlEnableParams = Schema.Struct({ + ephemeral: Schema.optionalKey(Schema.Boolean), +}); + export type ClientRequest__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly phase?: ClientRequest__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -30523,12 +33480,16 @@ export type ClientRequest__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -30536,6 +33497,7 @@ export type ClientRequest__ResponseItem = readonly action: ClientRequest__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status: ClientRequest__LocalShellStatus; readonly type: "local_shell_call"; } @@ -30543,6 +33505,7 @@ export type ClientRequest__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -30552,11 +33515,14 @@ export type ClientRequest__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly output: ClientRequest__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -30564,12 +33530,16 @@ export type ClientRequest__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: ClientRequest__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -30577,6 +33547,8 @@ export type ClientRequest__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -30584,26 +33556,39 @@ export type ClientRequest__ResponseItem = | { readonly action?: ClientRequest__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: ClientRequest__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const ClientRequest__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(ClientRequest__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), phase: Schema.optionalKey(Schema.Union([ClientRequest__MessagePhase, Schema.Null])), role: Schema.String, @@ -30612,6 +33597,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ author: Schema.String, content: Schema.Array(ClientRequest__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -30620,6 +33609,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Union([Schema.Array(ClientRequest__ReasoningItemContent), Schema.Null]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), summary: Schema.Array(ClientRequest__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -30635,11 +33628,13 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: ClientRequest__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -30648,8 +33643,9 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -30659,8 +33655,9 @@ export const ClientRequest__ResponseItem = Schema.Union( arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -30669,6 +33666,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), output: ClientRequest__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -30676,11 +33677,13 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -30688,6 +33691,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: ClientRequest__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -30697,6 +33704,10 @@ export const ClientRequest__ResponseItem = Schema.Union( Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -30707,14 +33718,18 @@ export const ClientRequest__ResponseItem = Schema.Union( action: Schema.optionalKey( Schema.Union([ClientRequest__ResponsesApiWebSearchAction, Schema.Null]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -30724,6 +33739,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -30733,6 +33752,10 @@ export const ClientRequest__ResponseItem = Schema.Union( }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([ClientRequest__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -30760,7 +33783,9 @@ export const ClientRequest__SelectedCapabilityRoot = Schema.Struct({ [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -30775,6 +33800,9 @@ export const ClientRequest__SelectedCapabilityRoot = Schema.Struct({ description: "A user-selected root that can expose one or more runtime capabilities.", }); +export type ClientRequest__ThreadHistoryMode = "legacy" | "paginated"; +export const ClientRequest__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type ClientRequest__ThreadMemoryMode = "enabled" | "disabled"; export const ClientRequest__ThreadMemoryMode = Schema.Literals(["enabled", "disabled"]); @@ -30804,6 +33832,17 @@ export const ClientRequest__ThreadRealtimeAudioChunk = Schema.Struct({ ), }).annotate({ description: "EXPERIMENTAL - thread realtime audio chunk." }); +export type ClientRequest__ThreadRealtimeInitialItem = { + readonly role: ClientRequest__ConversationTextRole; + readonly text: string; +}; +export const ClientRequest__ThreadRealtimeInitialItem = Schema.Struct({ + role: ClientRequest__ConversationTextRole, + text: Schema.String, +}).annotate({ + description: "EXPERIMENTAL - role-bearing text item included when a realtime V3 session starts.", +}); + export type ClientRequest__ThreadRealtimeStartTransport = | { readonly type: "websocket" } | { readonly sdp: string; readonly type: "webrtc" }; @@ -30852,19 +33891,29 @@ export const ClientRequest__ThreadResumeInitialTurnsPageParams = Schema.Struct({ }); export type ClientRequest__TurnEnvironmentParams = { - readonly cwd: ClientRequest__AbsolutePathBuf; + readonly cwd: ClientRequest__LegacyAppPathString; readonly environmentId: string; + readonly runtimeWorkspaceRoots?: ReadonlyArray | null; }; export const ClientRequest__TurnEnvironmentParams = Schema.Struct({ - cwd: ClientRequest__AbsolutePathBuf, + cwd: ClientRequest__LegacyAppPathString, environmentId: Schema.String, + runtimeWorkspaceRoots: Schema.optionalKey( + Schema.Union([ + Schema.Array(ClientRequest__LegacyAppPathString).annotate({ + description: "Environment-native runtime workspace roots. Omitted defaults to `cwd`.", + }), + Schema.Null, + ]), + ), }); export type CommandExecutionRequestApprovalParams = { readonly approvalId?: string | null; readonly command?: string | null; readonly commandActions?: ReadonlyArray | null; - readonly cwd?: CommandExecutionRequestApprovalParams__AbsolutePathBuf | null; + readonly cwd?: CommandExecutionRequestApprovalParams__LegacyAppPathString | null; + readonly environmentId?: string | null; readonly itemId: string; readonly networkApprovalContext?: CommandExecutionRequestApprovalParams__NetworkApprovalContext | null; readonly proposedExecpolicyAmendment?: ReadonlyArray | null; @@ -30899,9 +33948,16 @@ export const CommandExecutionRequestApprovalParams = Schema.Struct({ ]), ), cwd: Schema.optionalKey( - Schema.Union([CommandExecutionRequestApprovalParams__AbsolutePathBuf, Schema.Null]).annotate({ - description: "The command's working directory.", - }), + Schema.Union([ + CommandExecutionRequestApprovalParams__LegacyAppPathString, + Schema.Null, + ]).annotate({ description: "The command's working directory." }), + ), + environmentId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ description: "Environment in which the command will run." }), + Schema.Null, + ]), ), itemId: Schema.String, networkApprovalContext: Schema.optionalKey( @@ -31283,6 +34339,15 @@ export type McpServerElicitationRequestParams = readonly threadId: string; readonly turnId?: string | null; } + | { + readonly _meta?: unknown; + readonly message: string; + readonly mode: "openai/form"; + readonly requestedSchema: unknown; + readonly serverName: string; + readonly threadId: string; + readonly turnId?: string | null; + } | { readonly _meta?: unknown; readonly elicitationId: string; @@ -31312,6 +34377,23 @@ export const McpServerElicitationRequestParams = Schema.Union( ]), ), }).annotate({ title: "McpServerElicitationRequestParams" }), + Schema.Struct({ + _meta: Schema.optionalKey(Schema.Unknown), + message: Schema.String, + mode: Schema.Literal("openai/form"), + requestedSchema: Schema.Unknown, + serverName: Schema.String, + threadId: Schema.String, + turnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + }), + Schema.Null, + ]), + ), + }).annotate({ title: "McpServerElicitationRequestParams" }), Schema.Struct({ _meta: Schema.optionalKey(Schema.Unknown), elicitationId: Schema.String, @@ -31410,677 +34492,1469 @@ export const RequestId = Schema.Union([ ]).annotate({ title: "RequestId" }); export type ServerNotification = - | { readonly method: "error"; readonly params: ServerNotification__ErrorNotification } + | { + readonly method: "error"; + readonly params: ServerNotification__ErrorNotification; + readonly emittedAtMs?: number; + } | { readonly method: "thread/started"; readonly params: ServerNotification__ThreadStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/status/changed"; readonly params: ServerNotification__ThreadStatusChangedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/archived"; readonly params: ServerNotification__ThreadArchivedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/deleted"; readonly params: ServerNotification__ThreadDeletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/unarchived"; readonly params: ServerNotification__ThreadUnarchivedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/closed"; readonly params: ServerNotification__ThreadClosedNotification; + readonly emittedAtMs?: number; } | { readonly method: "skills/changed"; readonly params: ServerNotification__SkillsChangedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/name/updated"; readonly params: ServerNotification__ThreadNameUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/goal/updated"; readonly params: ServerNotification__ThreadGoalUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/goal/cleared"; readonly params: ServerNotification__ThreadGoalClearedNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "thread/environment/connected"; + readonly params: ServerNotification__EnvironmentConnectionNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "thread/environment/disconnected"; + readonly params: ServerNotification__EnvironmentConnectionNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/settings/updated"; readonly params: ServerNotification__ThreadSettingsUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/tokenUsage/updated"; readonly params: ServerNotification__ThreadTokenUsageUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "turn/started"; readonly params: ServerNotification__TurnStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "hook/started"; readonly params: ServerNotification__HookStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "turn/completed"; readonly params: ServerNotification__TurnCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "hook/completed"; readonly params: ServerNotification__HookCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "turn/diff/updated"; readonly params: ServerNotification__TurnDiffUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "turn/plan/updated"; readonly params: ServerNotification__TurnPlanUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/started"; readonly params: ServerNotification__ItemStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/autoApprovalReview/started"; readonly params: ServerNotification__ItemGuardianApprovalReviewStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/autoApprovalReview/completed"; readonly params: ServerNotification__ItemGuardianApprovalReviewCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/completed"; readonly params: ServerNotification__ItemCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/agentMessage/delta"; readonly params: ServerNotification__AgentMessageDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/plan/delta"; readonly params: ServerNotification__PlanDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "command/exec/outputDelta"; readonly params: ServerNotification__CommandExecOutputDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "process/outputDelta"; readonly params: ServerNotification__ProcessOutputDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "process/exited"; readonly params: ServerNotification__ProcessExitedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/commandExecution/outputDelta"; readonly params: ServerNotification__CommandExecutionOutputDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/commandExecution/terminalInteraction"; readonly params: ServerNotification__TerminalInteractionNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/fileChange/outputDelta"; readonly params: ServerNotification__FileChangeOutputDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/fileChange/patchUpdated"; readonly params: ServerNotification__FileChangePatchUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "serverRequest/resolved"; readonly params: ServerNotification__ServerRequestResolvedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/mcpToolCall/progress"; readonly params: ServerNotification__McpToolCallProgressNotification; + readonly emittedAtMs?: number; } | { readonly method: "mcpServer/oauthLogin/completed"; readonly params: ServerNotification__McpServerOauthLoginCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "mcpServer/startupStatus/updated"; readonly params: ServerNotification__McpServerStatusUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "account/updated"; readonly params: ServerNotification__AccountUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "account/rateLimits/updated"; readonly params: ServerNotification__AccountRateLimitsUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "app/list/updated"; readonly params: ServerNotification__AppListUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "remoteControl/status/changed"; readonly params: ServerNotification__RemoteControlStatusChangedNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "externalAgentConfig/import/progress"; + readonly params: ServerNotification__ExternalAgentConfigImportProgressNotification; + readonly emittedAtMs?: number; } | { readonly method: "externalAgentConfig/import/completed"; readonly params: ServerNotification__ExternalAgentConfigImportCompletedNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "fs/changed"; + readonly params: ServerNotification__FsChangedNotification; + readonly emittedAtMs?: number; } - | { readonly method: "fs/changed"; readonly params: ServerNotification__FsChangedNotification } | { readonly method: "item/reasoning/summaryTextDelta"; readonly params: ServerNotification__ReasoningSummaryTextDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/reasoning/summaryPartAdded"; readonly params: ServerNotification__ReasoningSummaryPartAddedNotification; + readonly emittedAtMs?: number; } | { readonly method: "item/reasoning/textDelta"; readonly params: ServerNotification__ReasoningTextDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/compacted"; readonly params: ServerNotification__ContextCompactedNotification; + readonly emittedAtMs?: number; } | { readonly method: "model/rerouted"; readonly params: ServerNotification__ModelReroutedNotification; + readonly emittedAtMs?: number; } | { readonly method: "model/verification"; readonly params: ServerNotification__ModelVerificationNotification; + readonly emittedAtMs?: number; } | { readonly method: "turn/moderationMetadata"; readonly params: ServerNotification__TurnModerationMetadataNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "model/safetyBuffering/updated"; + readonly params: ServerNotification__ModelSafetyBufferingUpdatedNotification; + readonly emittedAtMs?: number; + } + | { + readonly method: "warning"; + readonly params: ServerNotification__WarningNotification; + readonly emittedAtMs?: number; } - | { readonly method: "warning"; readonly params: ServerNotification__WarningNotification } | { readonly method: "guardianWarning"; readonly params: ServerNotification__GuardianWarningNotification; + readonly emittedAtMs?: number; } | { readonly method: "deprecationNotice"; readonly params: ServerNotification__DeprecationNoticeNotification; + readonly emittedAtMs?: number; } | { readonly method: "configWarning"; readonly params: ServerNotification__ConfigWarningNotification; + readonly emittedAtMs?: number; } | { readonly method: "fuzzyFileSearch/sessionUpdated"; readonly params: ServerNotification__FuzzyFileSearchSessionUpdatedNotification; + readonly emittedAtMs?: number; } | { readonly method: "fuzzyFileSearch/sessionCompleted"; readonly params: ServerNotification__FuzzyFileSearchSessionCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/started"; readonly params: ServerNotification__ThreadRealtimeStartedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/itemAdded"; readonly params: ServerNotification__ThreadRealtimeItemAddedNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/transcript/delta"; readonly params: ServerNotification__ThreadRealtimeTranscriptDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/transcript/done"; readonly params: ServerNotification__ThreadRealtimeTranscriptDoneNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/outputAudio/delta"; readonly params: ServerNotification__ThreadRealtimeOutputAudioDeltaNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/sdp"; readonly params: ServerNotification__ThreadRealtimeSdpNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/error"; readonly params: ServerNotification__ThreadRealtimeErrorNotification; + readonly emittedAtMs?: number; } | { readonly method: "thread/realtime/closed"; readonly params: ServerNotification__ThreadRealtimeClosedNotification; + readonly emittedAtMs?: number; } | { readonly method: "windows/worldWritableWarning"; readonly params: ServerNotification__WindowsWorldWritableWarningNotification; + readonly emittedAtMs?: number; } | { readonly method: "windowsSandbox/setupCompleted"; readonly params: ServerNotification__WindowsSandboxSetupCompletedNotification; + readonly emittedAtMs?: number; } | { readonly method: "account/login/completed"; readonly params: ServerNotification__AccountLoginCompletedNotification; + readonly emittedAtMs?: number; }; export const ServerNotification = Schema.Union( [ Schema.Struct({ method: Schema.Literal("error").annotate({ title: "ErrorNotificationMethod" }), params: ServerNotification__ErrorNotification, - }).annotate({ title: "ErrorNotification", description: "NEW NOTIFICATIONS" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/started").annotate({ title: "Thread/startedNotificationMethod", }), params: ServerNotification__ThreadStartedNotification, - }).annotate({ title: "Thread/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/status/changed").annotate({ title: "Thread/status/changedNotificationMethod", }), params: ServerNotification__ThreadStatusChangedNotification, - }).annotate({ title: "Thread/status/changedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/archived").annotate({ title: "Thread/archivedNotificationMethod", }), params: ServerNotification__ThreadArchivedNotification, - }).annotate({ title: "Thread/archivedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/deleted").annotate({ title: "Thread/deletedNotificationMethod", }), params: ServerNotification__ThreadDeletedNotification, - }).annotate({ title: "Thread/deletedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/unarchived").annotate({ title: "Thread/unarchivedNotificationMethod", }), params: ServerNotification__ThreadUnarchivedNotification, - }).annotate({ title: "Thread/unarchivedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/closed").annotate({ title: "Thread/closedNotificationMethod", }), params: ServerNotification__ThreadClosedNotification, - }).annotate({ title: "Thread/closedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("skills/changed").annotate({ title: "Skills/changedNotificationMethod", }), params: ServerNotification__SkillsChangedNotification, - }).annotate({ title: "Skills/changedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/name/updated").annotate({ title: "Thread/name/updatedNotificationMethod", }), params: ServerNotification__ThreadNameUpdatedNotification, - }).annotate({ title: "Thread/name/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/goal/updated").annotate({ title: "Thread/goal/updatedNotificationMethod", }), params: ServerNotification__ThreadGoalUpdatedNotification, - }).annotate({ title: "Thread/goal/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/goal/cleared").annotate({ title: "Thread/goal/clearedNotificationMethod", }), params: ServerNotification__ThreadGoalClearedNotification, - }).annotate({ title: "Thread/goal/clearedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), + Schema.Struct({ + method: Schema.Literal("thread/environment/connected").annotate({ + title: "Thread/environment/connectedNotificationMethod", + }), + params: ServerNotification__EnvironmentConnectionNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), + Schema.Struct({ + method: Schema.Literal("thread/environment/disconnected").annotate({ + title: "Thread/environment/disconnectedNotificationMethod", + }), + params: ServerNotification__EnvironmentConnectionNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/settings/updated").annotate({ title: "Thread/settings/updatedNotificationMethod", }), params: ServerNotification__ThreadSettingsUpdatedNotification, - }).annotate({ title: "Thread/settings/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/tokenUsage/updated").annotate({ title: "Thread/tokenUsage/updatedNotificationMethod", }), params: ServerNotification__ThreadTokenUsageUpdatedNotification, - }).annotate({ title: "Thread/tokenUsage/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("turn/started").annotate({ title: "Turn/startedNotificationMethod" }), params: ServerNotification__TurnStartedNotification, - }).annotate({ title: "Turn/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("hook/started").annotate({ title: "Hook/startedNotificationMethod" }), params: ServerNotification__HookStartedNotification, - }).annotate({ title: "Hook/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("turn/completed").annotate({ title: "Turn/completedNotificationMethod", }), params: ServerNotification__TurnCompletedNotification, - }).annotate({ title: "Turn/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("hook/completed").annotate({ title: "Hook/completedNotificationMethod", }), params: ServerNotification__HookCompletedNotification, - }).annotate({ title: "Hook/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("turn/diff/updated").annotate({ title: "Turn/diff/updatedNotificationMethod", }), params: ServerNotification__TurnDiffUpdatedNotification, - }).annotate({ title: "Turn/diff/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("turn/plan/updated").annotate({ title: "Turn/plan/updatedNotificationMethod", }), params: ServerNotification__TurnPlanUpdatedNotification, - }).annotate({ title: "Turn/plan/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/started").annotate({ title: "Item/startedNotificationMethod" }), params: ServerNotification__ItemStartedNotification, - }).annotate({ title: "Item/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/autoApprovalReview/started").annotate({ title: "Item/autoApprovalReview/startedNotificationMethod", }), params: ServerNotification__ItemGuardianApprovalReviewStartedNotification, - }).annotate({ title: "Item/autoApprovalReview/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/autoApprovalReview/completed").annotate({ title: "Item/autoApprovalReview/completedNotificationMethod", }), params: ServerNotification__ItemGuardianApprovalReviewCompletedNotification, - }).annotate({ title: "Item/autoApprovalReview/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/completed").annotate({ title: "Item/completedNotificationMethod", }), params: ServerNotification__ItemCompletedNotification, - }).annotate({ title: "Item/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/agentMessage/delta").annotate({ title: "Item/agentMessage/deltaNotificationMethod", }), params: ServerNotification__AgentMessageDeltaNotification, - }).annotate({ title: "Item/agentMessage/deltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/plan/delta").annotate({ title: "Item/plan/deltaNotificationMethod", }), params: ServerNotification__PlanDeltaNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Item/plan/deltaNotification", - description: "EXPERIMENTAL - proposed plan streaming deltas for plan items.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("command/exec/outputDelta").annotate({ title: "Command/exec/outputDeltaNotificationMethod", }), params: ServerNotification__CommandExecOutputDeltaNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Command/exec/outputDeltaNotification", - description: - "Stream base64-encoded stdout/stderr chunks for a running `command/exec` session.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("process/outputDelta").annotate({ title: "Process/outputDeltaNotificationMethod", }), params: ServerNotification__ProcessOutputDeltaNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Process/outputDeltaNotification", - description: - "Stream base64-encoded stdout/stderr chunks for a running `process/spawn` session.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("process/exited").annotate({ title: "Process/exitedNotificationMethod", }), params: ServerNotification__ProcessExitedNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Process/exitedNotification", - description: "Final exit notification for a `process/spawn` session.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("item/commandExecution/outputDelta").annotate({ title: "Item/commandExecution/outputDeltaNotificationMethod", }), params: ServerNotification__CommandExecutionOutputDeltaNotification, - }).annotate({ title: "Item/commandExecution/outputDeltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/commandExecution/terminalInteraction").annotate({ title: "Item/commandExecution/terminalInteractionNotificationMethod", }), params: ServerNotification__TerminalInteractionNotification, - }).annotate({ title: "Item/commandExecution/terminalInteractionNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/fileChange/outputDelta").annotate({ title: "Item/fileChange/outputDeltaNotificationMethod", }), params: ServerNotification__FileChangeOutputDeltaNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Item/fileChange/outputDeltaNotification", - description: "Deprecated legacy apply_patch output stream notification.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("item/fileChange/patchUpdated").annotate({ title: "Item/fileChange/patchUpdatedNotificationMethod", }), params: ServerNotification__FileChangePatchUpdatedNotification, - }).annotate({ title: "Item/fileChange/patchUpdatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("serverRequest/resolved").annotate({ title: "ServerRequest/resolvedNotificationMethod", }), params: ServerNotification__ServerRequestResolvedNotification, - }).annotate({ title: "ServerRequest/resolvedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/mcpToolCall/progress").annotate({ title: "Item/mcpToolCall/progressNotificationMethod", }), params: ServerNotification__McpToolCallProgressNotification, - }).annotate({ title: "Item/mcpToolCall/progressNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("mcpServer/oauthLogin/completed").annotate({ title: "McpServer/oauthLogin/completedNotificationMethod", }), params: ServerNotification__McpServerOauthLoginCompletedNotification, - }).annotate({ title: "McpServer/oauthLogin/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("mcpServer/startupStatus/updated").annotate({ title: "McpServer/startupStatus/updatedNotificationMethod", }), params: ServerNotification__McpServerStatusUpdatedNotification, - }).annotate({ title: "McpServer/startupStatus/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("account/updated").annotate({ title: "Account/updatedNotificationMethod", }), params: ServerNotification__AccountUpdatedNotification, - }).annotate({ title: "Account/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("account/rateLimits/updated").annotate({ title: "Account/rateLimits/updatedNotificationMethod", }), params: ServerNotification__AccountRateLimitsUpdatedNotification, - }).annotate({ title: "Account/rateLimits/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("app/list/updated").annotate({ title: "App/list/updatedNotificationMethod", }), params: ServerNotification__AppListUpdatedNotification, - }).annotate({ title: "App/list/updatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("remoteControl/status/changed").annotate({ title: "RemoteControl/status/changedNotificationMethod", }), params: ServerNotification__RemoteControlStatusChangedNotification, - }).annotate({ title: "RemoteControl/status/changedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), + Schema.Struct({ + method: Schema.Literal("externalAgentConfig/import/progress").annotate({ + title: "ExternalAgentConfig/import/progressNotificationMethod", + }), + params: ServerNotification__ExternalAgentConfigImportProgressNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("externalAgentConfig/import/completed").annotate({ title: "ExternalAgentConfig/import/completedNotificationMethod", }), params: ServerNotification__ExternalAgentConfigImportCompletedNotification, - }).annotate({ title: "ExternalAgentConfig/import/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("fs/changed").annotate({ title: "Fs/changedNotificationMethod" }), params: ServerNotification__FsChangedNotification, - }).annotate({ title: "Fs/changedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/reasoning/summaryTextDelta").annotate({ title: "Item/reasoning/summaryTextDeltaNotificationMethod", }), params: ServerNotification__ReasoningSummaryTextDeltaNotification, - }).annotate({ title: "Item/reasoning/summaryTextDeltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/reasoning/summaryPartAdded").annotate({ title: "Item/reasoning/summaryPartAddedNotificationMethod", }), params: ServerNotification__ReasoningSummaryPartAddedNotification, - }).annotate({ title: "Item/reasoning/summaryPartAddedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("item/reasoning/textDelta").annotate({ title: "Item/reasoning/textDeltaNotificationMethod", }), params: ServerNotification__ReasoningTextDeltaNotification, - }).annotate({ title: "Item/reasoning/textDeltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/compacted").annotate({ title: "Thread/compactedNotificationMethod", }), params: ServerNotification__ContextCompactedNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Thread/compactedNotification", - description: "Deprecated: Use `ContextCompaction` item type instead.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("model/rerouted").annotate({ title: "Model/reroutedNotificationMethod", }), params: ServerNotification__ModelReroutedNotification, - }).annotate({ title: "Model/reroutedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("model/verification").annotate({ title: "Model/verificationNotificationMethod", }), params: ServerNotification__ModelVerificationNotification, - }).annotate({ title: "Model/verificationNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("turn/moderationMetadata").annotate({ title: "Turn/moderationMetadataNotificationMethod", }), params: ServerNotification__TurnModerationMetadataNotification, - }).annotate({ title: "Turn/moderationMetadataNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), + Schema.Struct({ + method: Schema.Literal("model/safetyBuffering/updated").annotate({ + title: "Model/safetyBuffering/updatedNotificationMethod", + }), + params: ServerNotification__ModelSafetyBufferingUpdatedNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("warning").annotate({ title: "WarningNotificationMethod" }), params: ServerNotification__WarningNotification, - }).annotate({ title: "WarningNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("guardianWarning").annotate({ title: "GuardianWarningNotificationMethod", }), params: ServerNotification__GuardianWarningNotification, - }).annotate({ title: "GuardianWarningNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("deprecationNotice").annotate({ title: "DeprecationNoticeNotificationMethod", }), params: ServerNotification__DeprecationNoticeNotification, - }).annotate({ title: "DeprecationNoticeNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("configWarning").annotate({ title: "ConfigWarningNotificationMethod", }), params: ServerNotification__ConfigWarningNotification, - }).annotate({ title: "ConfigWarningNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("fuzzyFileSearch/sessionUpdated").annotate({ title: "FuzzyFileSearch/sessionUpdatedNotificationMethod", }), params: ServerNotification__FuzzyFileSearchSessionUpdatedNotification, - }).annotate({ title: "FuzzyFileSearch/sessionUpdatedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("fuzzyFileSearch/sessionCompleted").annotate({ title: "FuzzyFileSearch/sessionCompletedNotificationMethod", }), params: ServerNotification__FuzzyFileSearchSessionCompletedNotification, - }).annotate({ title: "FuzzyFileSearch/sessionCompletedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/started").annotate({ title: "Thread/realtime/startedNotificationMethod", }), params: ServerNotification__ThreadRealtimeStartedNotification, - }).annotate({ title: "Thread/realtime/startedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/itemAdded").annotate({ title: "Thread/realtime/itemAddedNotificationMethod", }), params: ServerNotification__ThreadRealtimeItemAddedNotification, - }).annotate({ title: "Thread/realtime/itemAddedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/transcript/delta").annotate({ title: "Thread/realtime/transcript/deltaNotificationMethod", }), params: ServerNotification__ThreadRealtimeTranscriptDeltaNotification, - }).annotate({ title: "Thread/realtime/transcript/deltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/transcript/done").annotate({ title: "Thread/realtime/transcript/doneNotificationMethod", }), params: ServerNotification__ThreadRealtimeTranscriptDoneNotification, - }).annotate({ title: "Thread/realtime/transcript/doneNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/outputAudio/delta").annotate({ title: "Thread/realtime/outputAudio/deltaNotificationMethod", }), params: ServerNotification__ThreadRealtimeOutputAudioDeltaNotification, - }).annotate({ title: "Thread/realtime/outputAudio/deltaNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/sdp").annotate({ title: "Thread/realtime/sdpNotificationMethod", }), params: ServerNotification__ThreadRealtimeSdpNotification, - }).annotate({ title: "Thread/realtime/sdpNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/error").annotate({ title: "Thread/realtime/errorNotificationMethod", }), params: ServerNotification__ThreadRealtimeErrorNotification, - }).annotate({ title: "Thread/realtime/errorNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("thread/realtime/closed").annotate({ title: "Thread/realtime/closedNotificationMethod", }), params: ServerNotification__ThreadRealtimeClosedNotification, - }).annotate({ title: "Thread/realtime/closedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("windows/worldWritableWarning").annotate({ title: "Windows/worldWritableWarningNotificationMethod", }), params: ServerNotification__WindowsWorldWritableWarningNotification, + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), }).annotate({ - title: "Windows/worldWritableWarningNotification", - description: - "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", + title: "ServerNotification", + description: "Notification sent from the server to the client.", }), Schema.Struct({ method: Schema.Literal("windowsSandbox/setupCompleted").annotate({ title: "WindowsSandbox/setupCompletedNotificationMethod", }), params: ServerNotification__WindowsSandboxSetupCompletedNotification, - }).annotate({ title: "WindowsSandbox/setupCompletedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), Schema.Struct({ method: Schema.Literal("account/login/completed").annotate({ title: "Account/login/completedNotificationMethod", }), params: ServerNotification__AccountLoginCompletedNotification, - }).annotate({ title: "Account/login/completedNotification" }), + emittedAtMs: Schema.optionalKey( + Schema.Number.annotate({ + description: + "Unix timestamp (in milliseconds) when app-server emitted this notification.", + format: "int64", + }).check(Schema.isInt()), + ), + }).annotate({ + title: "ServerNotification", + description: "Notification sent from the server to the client.", + }), ], { mode: "oneOf" }, -).annotate({ - title: "ServerNotification", - description: "Notification sent from the server to the client.", -}); +); export type ServerNotification__ByteRange = { readonly end: number; readonly start: number }; export const ServerNotification__ByteRange = Schema.Struct({ @@ -32157,6 +36031,21 @@ export const ServerNotification__HookSource = Schema.Literals([ "unknown", ]); +export type ServerNotification__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const ServerNotification__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type ServerNotification__NetworkAccess = "restricted" | "enabled"; export const ServerNotification__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -32185,6 +36074,14 @@ export const ServerNotification__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type ServerNotification__ThreadExtra = {}; +export const ServerNotification__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type ServerNotification__ThreadHistoryMode = "legacy" | "paginated"; +export const ServerNotification__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type ServerNotification__TurnItemsView = "notLoaded" | "summary" | "full"; export const ServerNotification__TurnItemsView = Schema.Literals(["notLoaded", "summary", "full"]); @@ -32412,12 +36309,21 @@ export const ServerRequest__CommandExecutionApprovalDecision = Schema.Union( ); export type ToolRequestUserInputParams = { + readonly autoResolutionMs?: number | null; readonly itemId: string; readonly questions: ReadonlyArray; readonly threadId: string; readonly turnId: string; }; export const ToolRequestUserInputParams = Schema.Struct({ + autoResolutionMs: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ format: "uint64" }) + .check(Schema.isInt()) + .check(Schema.isGreaterThanOrEqualTo(0)), + Schema.Null, + ]), + ), itemId: Schema.String, questions: Schema.Array(ToolRequestUserInputParams__ToolRequestUserInputQuestion), threadId: Schema.String, @@ -32532,6 +36438,40 @@ export const V2AppListUpdatedNotification = Schema.Struct({ description: "EXPERIMENTAL - notification emitted when the app list changes.", }); +export type V2AppsInstalledParams = { + readonly forceRefresh?: boolean; + readonly threadId?: string | null; +}; +export const V2AppsInstalledParams = Schema.Struct({ + forceRefresh: Schema.optionalKey( + Schema.Boolean.annotate({ + description: + "When true and Apps are permitted, refresh and publish the hosted connector runtime tool snapshot first.", + }), + ), + threadId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Optional loaded thread id used to evaluate effective app configuration.", + }), + Schema.Null, + ]), + ), +}).annotate({ + title: "AppsInstalledParams", + description: "Read the committed installed connector runtime snapshot.", +}); + +export type V2AppsInstalledResponse = { + readonly apps: ReadonlyArray; +}; +export const V2AppsInstalledResponse = Schema.Struct({ + apps: Schema.Array(V2AppsInstalledResponse__InstalledApp), +}).annotate({ + title: "AppsInstalledResponse", + description: "The installed connectors in one committed runtime snapshot.", +}); + export type V2AppsListParams = { readonly cursor?: string | null; readonly forceRefetch?: boolean; @@ -32594,6 +36534,35 @@ export const V2AppsListResponse = Schema.Struct({ ), }).annotate({ title: "AppsListResponse", description: "EXPERIMENTAL - app list response." }); +export type V2AppsReadParams = { + readonly appIds: ReadonlyArray; + readonly includeTools?: boolean; +}; +export const V2AppsReadParams = Schema.Struct({ + appIds: Schema.Array(Schema.String).annotate({ + description: + "App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while preserving their first-request order.", + }), + includeTools: Schema.optionalKey( + Schema.Boolean.annotate({ + description: + "When true, include display-only public tool summaries in the returned metadata.", + }), + ), +}).annotate({ + title: "AppsReadParams", + description: "EXPERIMENTAL - read metadata for specific apps/connectors.", +}); + +export type V2AppsReadResponse = { + readonly apps: ReadonlyArray; + readonly missingAppIds: ReadonlyArray; +}; +export const V2AppsReadResponse = Schema.Struct({ + apps: Schema.Array(V2AppsReadResponse__ConnectorMetadata), + missingAppIds: Schema.Array(Schema.String), +}).annotate({ title: "AppsReadResponse", description: "EXPERIMENTAL - app/read response." }); + export type V2CancelLoginAccountParams = { readonly loginId: string }; export const V2CancelLoginAccountParams = Schema.Struct({ loginId: Schema.String }).annotate({ title: "CancelLoginAccountParams", @@ -33017,6 +36986,7 @@ export type V2ConfigRequirementsReadResponse__ManagedHooksRequirements = { readonly PostToolUse: ReadonlyArray; readonly PreCompact: ReadonlyArray; readonly PreToolUse: ReadonlyArray; + readonly SessionEnd?: ReadonlyArray; readonly SessionStart: ReadonlyArray; readonly Stop: ReadonlyArray; readonly SubagentStart: ReadonlyArray; @@ -33031,6 +37001,11 @@ export const V2ConfigRequirementsReadResponse__ManagedHooksRequirements = Schema PostToolUse: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), PreCompact: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), PreToolUse: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), + SessionEnd: Schema.optionalKey( + Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup).annotate({ + default: [], + }), + ), SessionStart: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), Stop: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), SubagentStart: Schema.Array(V2ConfigRequirementsReadResponse__ConfiguredHookMatcherGroup), @@ -33206,6 +37181,33 @@ export const V2ConfigWriteResponse = Schema.Struct({ version: Schema.String, }).annotate({ title: "ConfigWriteResponse" }); +export type V2ConsumeAccountRateLimitResetCreditParams = { + readonly creditId?: string | null; + readonly idempotencyKey: string; +}; +export const V2ConsumeAccountRateLimitResetCreditParams = Schema.Struct({ + creditId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + }), + Schema.Null, + ]), + ), + idempotencyKey: Schema.String.annotate({ + description: + "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + }), +}).annotate({ title: "ConsumeAccountRateLimitResetCreditParams" }); + +export type V2ConsumeAccountRateLimitResetCreditResponse = { + readonly outcome: V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome; +}; +export const V2ConsumeAccountRateLimitResetCreditResponse = Schema.Struct({ + outcome: V2ConsumeAccountRateLimitResetCreditResponse__ConsumeAccountRateLimitResetCreditOutcome, +}).annotate({ title: "ConsumeAccountRateLimitResetCreditResponse" }); + export type V2ContextCompactedNotification = { readonly threadId: string; readonly turnId: string }; export const V2ContextCompactedNotification = Schema.Struct({ threadId: Schema.String, @@ -33231,6 +37233,15 @@ export const V2DeprecationNoticeNotification = Schema.Struct({ summary: Schema.String.annotate({ description: "Concise summary of what is deprecated." }), }).annotate({ title: "DeprecationNoticeNotification" }); +export type V2EnvironmentConnectionNotification = { + readonly environmentId: string; + readonly threadId: string; +}; +export const V2EnvironmentConnectionNotification = Schema.Struct({ + environmentId: Schema.String, + threadId: Schema.String, +}).annotate({ title: "EnvironmentConnectionNotification" }); + export type V2ErrorNotification = { readonly error: V2ErrorNotification__TurnError; readonly threadId: string; @@ -33333,6 +37344,8 @@ export const V2ExperimentalFeatureListResponse__ExperimentalFeatureStage = Schem export type V2ExternalAgentConfigDetectParams = { readonly cwds?: ReadonlyArray | null; readonly includeHome?: boolean; + readonly migrationSource?: string | null; + readonly source?: string | null; }; export const V2ExternalAgentConfigDetectParams = Schema.Struct({ cwds: Schema.optionalKey( @@ -33345,9 +37358,27 @@ export const V2ExternalAgentConfigDetectParams = Schema.Struct({ ), includeHome: Schema.optionalKey( Schema.Boolean.annotate({ - description: "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + description: "If true, include detection under the user's home directory.", }), ), + migrationSource: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional migration-source selector. Missing or unrecognized values use the default source.", + }), + Schema.Null, + ]), + ), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Deprecated field retained for compatibility. This field is ignored; use `migrationSource` to select the migration source.", + }), + Schema.Null, + ]), + ), }).annotate({ title: "ExternalAgentConfigDetectParams" }); export type V2ExternalAgentConfigDetectResponse = { @@ -33357,22 +37388,71 @@ export const V2ExternalAgentConfigDetectResponse = Schema.Struct({ items: Schema.Array(V2ExternalAgentConfigDetectResponse__ExternalAgentConfigMigrationItem), }).annotate({ title: "ExternalAgentConfigDetectResponse" }); -export type V2ExternalAgentConfigImportCompletedNotification = {}; -export const V2ExternalAgentConfigImportCompletedNotification = Schema.Struct({}).annotate({ - title: "ExternalAgentConfigImportCompletedNotification", -}); +export type V2ExternalAgentConfigImportCompletedNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportCompletedNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array( + V2ExternalAgentConfigImportCompletedNotification__ExternalAgentConfigImportTypeResult, + ), +}).annotate({ title: "ExternalAgentConfigImportCompletedNotification" }); + +export type V2ExternalAgentConfigImportHistoriesReadResponse = { + readonly connectors: ReadonlyArray; + readonly data: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportHistoriesReadResponse = Schema.Struct({ + connectors: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentImportedConnectorCandidate, + ), + data: Schema.Array( + V2ExternalAgentConfigImportHistoriesReadResponse__ExternalAgentConfigImportHistory, + ), +}).annotate({ title: "ExternalAgentConfigImportHistoriesReadResponse" }); export type V2ExternalAgentConfigImportParams = { readonly migrationItems: ReadonlyArray; + readonly migrationSource?: string | null; + readonly source?: string | null; }; export const V2ExternalAgentConfigImportParams = Schema.Struct({ migrationItems: Schema.Array(V2ExternalAgentConfigImportParams__ExternalAgentConfigMigrationItem), + migrationSource: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Migration-source selector used to produce the migration items. Pass the same value to detection and import; missing or unrecognized values use the default source.", + }), + Schema.Null, + ]), + ), + source: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: "Optional identifier for the product that initiated the import.", + }), + Schema.Null, + ]), + ), }).annotate({ title: "ExternalAgentConfigImportParams" }); -export type V2ExternalAgentConfigImportResponse = {}; -export const V2ExternalAgentConfigImportResponse = Schema.Struct({}).annotate({ - title: "ExternalAgentConfigImportResponse", -}); +export type V2ExternalAgentConfigImportProgressNotification = { + readonly importId: string; + readonly itemTypeResults: ReadonlyArray; +}; +export const V2ExternalAgentConfigImportProgressNotification = Schema.Struct({ + importId: Schema.String, + itemTypeResults: Schema.Array( + V2ExternalAgentConfigImportProgressNotification__ExternalAgentConfigImportTypeResult, + ), +}).annotate({ title: "ExternalAgentConfigImportProgressNotification" }); + +export type V2ExternalAgentConfigImportResponse = { readonly importId: string }; +export const V2ExternalAgentConfigImportResponse = Schema.Struct({ + importId: Schema.String, +}).annotate({ title: "ExternalAgentConfigImportResponse" }); export type V2FeedbackUploadParams = { readonly classification: string; @@ -33735,6 +37815,7 @@ export const V2GetAccountParams = Schema.Struct({ }).annotate({ title: "GetAccountParams" }); export type V2GetAccountRateLimitsResponse = { + readonly rateLimitResetCredits?: V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary | null; readonly rateLimits: { readonly credits?: V2GetAccountRateLimitsResponse__CreditsSnapshot | null; readonly individualLimit?: V2GetAccountRateLimitsResponse__SpendControlLimitSnapshot | null; @@ -33744,12 +37825,16 @@ export type V2GetAccountRateLimitsResponse = { readonly primary?: V2GetAccountRateLimitsResponse__RateLimitWindow | null; readonly rateLimitReachedType?: V2GetAccountRateLimitsResponse__RateLimitReachedType | null; readonly secondary?: V2GetAccountRateLimitsResponse__RateLimitWindow | null; + readonly spendControlReached?: boolean | null; }; readonly rateLimitsByLimitId?: { readonly [x: string]: V2GetAccountRateLimitsResponse__RateLimitSnapshot; } | null; }; export const V2GetAccountRateLimitsResponse = Schema.Struct({ + rateLimitResetCredits: Schema.optionalKey( + Schema.Union([V2GetAccountRateLimitsResponse__RateLimitResetCreditsSummary, Schema.Null]), + ), rateLimits: Schema.Struct({ credits: Schema.optionalKey( Schema.Union([V2GetAccountRateLimitsResponse__CreditsSnapshot, Schema.Null]), @@ -33771,6 +37856,15 @@ export const V2GetAccountRateLimitsResponse = Schema.Struct({ secondary: Schema.optionalKey( Schema.Union([V2GetAccountRateLimitsResponse__RateLimitWindow, Schema.Null]), ), + spendControlReached: Schema.optionalKey( + Schema.Union([ + Schema.Boolean.annotate({ + description: + "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + }), + Schema.Null, + ]), + ), }).annotate({ description: "Backward-compatible single-bucket view; mirrors the historical payload.", }), @@ -33807,6 +37901,19 @@ export const V2GetAccountTokenUsageResponse = Schema.Struct({ summary: V2GetAccountTokenUsageResponse__AccountTokenUsageSummary, }).annotate({ title: "GetAccountTokenUsageResponse" }); +export type V2GetWorkspaceMessagesResponse = { + readonly featureEnabled: boolean; + readonly messages: ReadonlyArray; +}; +export const V2GetWorkspaceMessagesResponse = Schema.Struct({ + featureEnabled: Schema.Boolean.annotate({ + description: "Whether the workspace-message backend route is available for this client.", + }), + messages: Schema.Array(V2GetWorkspaceMessagesResponse__WorkspaceMessage).annotate({ + description: "Active workspace messages returned by the backend.", + }), +}).annotate({ title: "GetWorkspaceMessagesResponse" }); + export type V2GuardianWarningNotification = { readonly message: string; readonly threadId: string }; export const V2GuardianWarningNotification = Schema.Struct({ message: Schema.String.annotate({ @@ -34161,14 +38268,20 @@ export const V2ListMcpServerStatusResponse = Schema.Struct({ export type V2LoginAccountParams = | { readonly apiKey: string; readonly type: "apiKey" } - | { readonly codexStreamlinedLogin?: boolean; readonly type: "chatgpt" } + | { + readonly appBrand?: V2LoginAccountParams__LoginAppBrand | null; + readonly codexStreamlinedLogin?: boolean; + readonly type: "chatgpt"; + readonly useHostedLoginSuccessPage?: boolean; + } | { readonly type: "chatgptDeviceCode" } | { readonly accessToken: string; readonly chatgptAccountId: string; readonly chatgptPlanType?: string | null; readonly type: "chatgptAuthTokens"; - }; + } + | { readonly apiKey: string; readonly region: string; readonly type: "amazonBedrock" }; export const V2LoginAccountParams = Schema.Union( [ Schema.Struct({ @@ -34176,8 +38289,12 @@ export const V2LoginAccountParams = Schema.Union( type: Schema.Literal("apiKey").annotate({ title: "ApiKeyv2::LoginAccountParamsType" }), }).annotate({ title: "ApiKeyv2::LoginAccountParams" }), Schema.Struct({ + appBrand: Schema.optionalKey( + Schema.Union([V2LoginAccountParams__LoginAppBrand, Schema.Null]), + ), codexStreamlinedLogin: Schema.optionalKey(Schema.Boolean), type: Schema.Literal("chatgpt").annotate({ title: "Chatgptv2::LoginAccountParamsType" }), + useHostedLoginSuccessPage: Schema.optionalKey(Schema.Boolean), }).annotate({ title: "Chatgptv2::LoginAccountParams" }), Schema.Struct({ type: Schema.Literal("chatgptDeviceCode").annotate({ @@ -34209,6 +38326,16 @@ export const V2LoginAccountParams = Schema.Union( description: "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", }), + Schema.Struct({ + apiKey: Schema.String, + region: Schema.String, + type: Schema.Literal("amazonBedrock").annotate({ + title: "AmazonBedrockv2::LoginAccountParamsType", + }), + }).annotate({ + title: "AmazonBedrockv2::LoginAccountParams", + description: "[UNSTABLE] Managed Amazon Bedrock login is experimental.", + }), ], { mode: "oneOf" }, ).annotate({ title: "LoginAccountParams" }); @@ -34222,7 +38349,8 @@ export type V2LoginAccountResponse = readonly userCode: string; readonly verificationUrl: string; } - | { readonly type: "chatgptAuthTokens" }; + | { readonly type: "chatgptAuthTokens" } + | { readonly type: "amazonBedrock" }; export const V2LoginAccountResponse = Schema.Union( [ Schema.Struct({ @@ -34253,6 +38381,11 @@ export const V2LoginAccountResponse = Schema.Union( title: "ChatgptAuthTokensv2::LoginAccountResponseType", }), }).annotate({ title: "ChatgptAuthTokensv2::LoginAccountResponse" }), + Schema.Struct({ + type: Schema.Literal("amazonBedrock").annotate({ + title: "AmazonBedrockv2::LoginAccountResponseType", + }), + }).annotate({ title: "AmazonBedrockv2::LoginAccountResponse" }), ], { mode: "oneOf" }, ).annotate({ title: "LoginAccountResponse" }); @@ -34338,21 +38471,25 @@ export type V2McpServerOauthLoginCompletedNotification = { readonly error?: string | null; readonly name: string; readonly success: boolean; + readonly threadId?: string | null; }; export const V2McpServerOauthLoginCompletedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), name: Schema.String, success: Schema.Boolean, + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), }).annotate({ title: "McpServerOauthLoginCompletedNotification" }); export type V2McpServerOauthLoginParams = { readonly name: string; readonly scopes?: ReadonlyArray | null; + readonly threadId?: string | null; readonly timeoutSecs?: number | null; }; export const V2McpServerOauthLoginParams = Schema.Struct({ name: Schema.String, scopes: Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), timeoutSecs: Schema.optionalKey( Schema.Union([Schema.Number.annotate({ format: "int64" }).check(Schema.isInt()), Schema.Null]), ), @@ -34370,12 +38507,19 @@ export const V2McpServerRefreshResponse = Schema.Struct({}).annotate({ export type V2McpServerStatusUpdatedNotification = { readonly error?: string | null; + readonly failureReason?: V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason | null; readonly name: string; readonly status: V2McpServerStatusUpdatedNotification__McpServerStartupState; readonly threadId?: string | null; }; export const V2McpServerStatusUpdatedNotification = Schema.Struct({ error: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + failureReason: Schema.optionalKey( + Schema.Union([ + V2McpServerStatusUpdatedNotification__McpServerStartupFailureReason, + Schema.Null, + ]), + ), name: Schema.String, status: V2McpServerStatusUpdatedNotification__McpServerStartupState, threadId: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -34505,6 +38649,25 @@ export const V2ModelReroutedNotification = Schema.Struct({ turnId: Schema.String, }).annotate({ title: "ModelReroutedNotification" }); +export type V2ModelSafetyBufferingUpdatedNotification = { + readonly fasterModel?: string | null; + readonly model: string; + readonly reasons: ReadonlyArray; + readonly showBufferingUi: boolean; + readonly threadId: string; + readonly turnId: string; + readonly useCases: ReadonlyArray; +}; +export const V2ModelSafetyBufferingUpdatedNotification = Schema.Struct({ + fasterModel: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + model: Schema.String, + reasons: Schema.Array(Schema.String), + showBufferingUi: Schema.Boolean, + threadId: Schema.String, + turnId: Schema.String, + useCases: Schema.Array(Schema.String), +}).annotate({ title: "ModelSafetyBufferingUpdatedNotification" }); + export type V2ModelVerificationNotification = { readonly threadId: string; readonly turnId: string; @@ -34905,6 +39068,25 @@ export const V2ProcessOutputDeltaNotification__ProcessOutputStream = Schema.Lite "stderr", ]).annotate({ description: "Stream label for `process/outputDelta` notifications." }); +export type V2RawResponseCompletedNotification = { + readonly responseId: string; + readonly threadId: string; + readonly turnId: string; + readonly usage?: V2RawResponseCompletedNotification__TokenUsageBreakdown | null; +}; +export const V2RawResponseCompletedNotification = Schema.Struct({ + responseId: Schema.String, + threadId: Schema.String, + turnId: Schema.String, + usage: Schema.optionalKey( + Schema.Union([V2RawResponseCompletedNotification__TokenUsageBreakdown, Schema.Null]), + ), +}).annotate({ + title: "RawResponseCompletedNotification", + description: + "Internal-only notification containing the exact usage from one upstream Responses API completion.", +}); + export type V2RawResponseItemCompletedNotification = { readonly item: V2RawResponseItemCompletedNotification__ResponseItem; readonly threadId: string; @@ -35226,6 +39408,7 @@ export type V2ThreadForkParams = { readonly cwd?: string | null; readonly developerInstructions?: string | null; readonly ephemeral?: boolean; + readonly lastTurnId?: string | null; readonly model?: string | null; readonly modelProvider?: string | null; readonly sandbox?: V2ThreadForkParams__SandboxMode | null; @@ -35250,6 +39433,15 @@ export const V2ThreadForkParams = Schema.Struct({ cwd: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), developerInstructions: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), ephemeral: Schema.optionalKey(Schema.Boolean), + lastTurnId: Schema.optionalKey( + Schema.Union([ + Schema.String.annotate({ + description: + "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.", + }), + Schema.Null, + ]), + ), model: Schema.optionalKey( Schema.Union([ Schema.String.annotate({ @@ -35284,7 +39476,7 @@ export type V2ThreadForkResponse = { readonly approvalPolicy: V2ThreadForkResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadForkResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadForkResponse__ReasoningEffort | null; @@ -35310,8 +39502,9 @@ export const V2ThreadForkResponse = Schema.Struct({ }), cwd: V2ThreadForkResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadForkResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadForkResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -35433,6 +39626,21 @@ export const V2ThreadForkResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadForkResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadForkResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadForkResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadForkResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -35498,6 +39706,14 @@ export const V2ThreadForkResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadForkResponse__ThreadExtra = {}; +export const V2ThreadForkResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadForkResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadForkResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadForkResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -35786,6 +40002,14 @@ export const V2ThreadListResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadListResponse__ThreadExtra = {}; +export const V2ThreadListResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadListResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadListResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadListResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -35957,6 +40181,17 @@ export const V2ThreadMetadataUpdateResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadMetadataUpdateResponse__ThreadExtra = {}; +export const V2ThreadMetadataUpdateResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadMetadataUpdateResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadMetadataUpdateResponse__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadMetadataUpdateResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36077,6 +40312,14 @@ export const V2ThreadReadResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadReadResponse__ThreadExtra = {}; +export const V2ThreadReadResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadReadResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadReadResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadReadResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36271,6 +40514,7 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly content: ReadonlyArray; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly phase?: V2ThreadResumeParams__MessagePhase | null; readonly role: string; readonly type: "message"; @@ -36278,12 +40522,16 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly author: string; readonly content: ReadonlyArray; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly recipient: string; readonly type: "agent_message"; } | { readonly content?: ReadonlyArray | null; readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly summary: ReadonlyArray; readonly type: "reasoning"; } @@ -36291,6 +40539,7 @@ export type V2ThreadResumeParams__ResponseItem = readonly action: V2ThreadResumeParams__LocalShellAction; readonly call_id?: string | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status: V2ThreadResumeParams__LocalShellStatus; readonly type: "local_shell_call"; } @@ -36298,6 +40547,7 @@ export type V2ThreadResumeParams__ResponseItem = readonly arguments: string; readonly call_id: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name: string; readonly namespace?: string | null; readonly type: "function_call"; @@ -36307,11 +40557,14 @@ export type V2ThreadResumeParams__ResponseItem = readonly call_id?: string | null; readonly execution: string; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "tool_search_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly output: V2ThreadResumeParams__FunctionCallOutputBody; readonly type: "function_call_output"; } @@ -36319,12 +40572,16 @@ export type V2ThreadResumeParams__ResponseItem = readonly call_id: string; readonly id?: string | null; readonly input: string; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name: string; + readonly namespace?: string | null; readonly status?: string | null; readonly type: "custom_tool_call"; } | { readonly call_id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly name?: string | null; readonly output: V2ThreadResumeParams__FunctionCallOutputBody; readonly type: "custom_tool_call_output"; @@ -36332,6 +40589,8 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly call_id?: string | null; readonly execution: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status: string; readonly tools: ReadonlyArray; readonly type: "tool_search_output"; @@ -36339,26 +40598,39 @@ export type V2ThreadResumeParams__ResponseItem = | { readonly action?: V2ThreadResumeParams__ResponsesApiWebSearchAction | null; readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly status?: string | null; readonly type: "web_search_call"; } | { - readonly id: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; readonly result: string; readonly revised_prompt?: string | null; readonly status: string; readonly type: "image_generation_call"; } - | { readonly encrypted_content: string; readonly type: "compaction" } + | { + readonly encrypted_content: string; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; + readonly type: "compaction"; + } | { readonly type: "compaction_trigger" } - | { readonly encrypted_content?: string | null; readonly type: "context_compaction" } + | { + readonly encrypted_content?: string | null; + readonly id?: string | null; + readonly internal_chat_message_metadata_passthrough?: V2ThreadResumeParams__InternalChatMessageMetadataPassthrough | null; + readonly type: "context_compaction"; + } | { readonly type: "other" }; export const V2ThreadResumeParams__ResponseItem = Schema.Union( [ Schema.Struct({ content: Schema.Array(V2ThreadResumeParams__ContentItem), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), phase: Schema.optionalKey(Schema.Union([V2ThreadResumeParams__MessagePhase, Schema.Null])), role: Schema.String, @@ -36367,6 +40639,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ author: Schema.String, content: Schema.Array(V2ThreadResumeParams__AgentMessageInputContent), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), recipient: Schema.String, type: Schema.Literal("agent_message").annotate({ title: "AgentMessageResponseItemType" }), }).annotate({ title: "AgentMessageResponseItem" }), @@ -36375,6 +40651,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Union([Schema.Array(V2ThreadResumeParams__ReasoningItemContent), Schema.Null]), ), encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), summary: Schema.Array(V2ThreadResumeParams__ReasoningItemReasoningSummary), type: Schema.Literal("reasoning").annotate({ title: "ReasoningResponseItemType" }), }).annotate({ title: "ReasoningResponseItem" }), @@ -36390,11 +40670,13 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Union([ Schema.String.annotate({ description: "Legacy id field retained for compatibility with older payloads.", - writeOnly: true, }), Schema.Null, ]), ), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: V2ThreadResumeParams__LocalShellStatus, type: Schema.Literal("local_shell_call").annotate({ title: "LocalShellCallResponseItemType", @@ -36403,8 +40685,9 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ arguments: Schema.String, call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), name: Schema.String, namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), @@ -36414,8 +40697,9 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( arguments: Schema.Unknown, call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("tool_search_call").annotate({ @@ -36424,6 +40708,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "ToolSearchCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), output: V2ThreadResumeParams__FunctionCallOutputBody, type: Schema.Literal("function_call_output").annotate({ title: "FunctionCallOutputResponseItemType", @@ -36431,11 +40719,13 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "FunctionCallOutputResponseItem" }), Schema.Struct({ call_id: Schema.String, - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), - ), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), input: Schema.String, + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.String, + namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("custom_tool_call").annotate({ title: "CustomToolCallResponseItemType", @@ -36443,6 +40733,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "CustomToolCallResponseItem" }), Schema.Struct({ call_id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), name: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), output: V2ThreadResumeParams__FunctionCallOutputBody, type: Schema.Literal("custom_tool_call_output").annotate({ @@ -36452,6 +40746,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( Schema.Struct({ call_id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), execution: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), status: Schema.String, tools: Schema.Array(Schema.Unknown), type: Schema.Literal("tool_search_output").annotate({ @@ -36462,14 +40760,18 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( action: Schema.optionalKey( Schema.Union([V2ThreadResumeParams__ResponsesApiWebSearchAction, Schema.Null]), ), - id: Schema.optionalKey( - Schema.Union([Schema.String.annotate({ writeOnly: true }), Schema.Null]), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), ), status: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), type: Schema.Literal("web_search_call").annotate({ title: "WebSearchCallResponseItemType" }), }).annotate({ title: "WebSearchCallResponseItem" }), Schema.Struct({ - id: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), result: Schema.String, revised_prompt: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), status: Schema.String, @@ -36479,6 +40781,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "ImageGenerationCallResponseItem" }), Schema.Struct({ encrypted_content: Schema.String, + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("compaction").annotate({ title: "CompactionResponseItemType" }), }).annotate({ title: "CompactionResponseItem" }), Schema.Struct({ @@ -36488,6 +40794,10 @@ export const V2ThreadResumeParams__ResponseItem = Schema.Union( }).annotate({ title: "CompactionTriggerResponseItem" }), Schema.Struct({ encrypted_content: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + id: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + internal_chat_message_metadata_passthrough: Schema.optionalKey( + Schema.Union([V2ThreadResumeParams__InternalChatMessageMetadataPassthrough, Schema.Null]), + ), type: Schema.Literal("context_compaction").annotate({ title: "ContextCompactionResponseItemType", }), @@ -36529,7 +40839,7 @@ export type V2ThreadResumeResponse = { readonly approvalPolicy: V2ThreadResumeResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadResumeResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadResumeResponse__ReasoningEffort | null; @@ -36555,8 +40865,9 @@ export const V2ThreadResumeResponse = Schema.Struct({ }), cwd: V2ThreadResumeResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadResumeResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadResumeResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -36684,6 +40995,21 @@ export const V2ThreadResumeResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadResumeResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadResumeResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadResumeResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadResumeResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -36749,6 +41075,14 @@ export const V2ThreadResumeResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadResumeResponse__ThreadExtra = {}; +export const V2ThreadResumeResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadResumeResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadResumeResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadResumeResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -36804,7 +41138,10 @@ export const V2ThreadRollbackParams = Schema.Struct({ .check(Schema.isInt()) .check(Schema.isGreaterThanOrEqualTo(0)), threadId: Schema.String, -}).annotate({ title: "ThreadRollbackParams" }); +}).annotate({ + title: "ThreadRollbackParams", + description: "DEPRECATED: `thread/rollback` will be removed soon.", +}); export type V2ThreadRollbackResponse = { readonly thread: { @@ -36822,6 +41159,7 @@ export type V2ThreadRollbackResponse = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -36890,7 +41228,9 @@ export const V2ThreadRollbackResponse = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -36918,6 +41258,15 @@ export const V2ThreadRollbackResponse = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -37050,6 +41399,7 @@ export type V2ThreadRollbackResponse__Thread = { readonly parentThreadId?: string | null; readonly path?: string | null; readonly preview: string; + readonly recencyAt?: number | null; readonly sessionId: string; readonly source: | "cli" @@ -37116,7 +41466,9 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ description: "Optional Git metadata captured when the thread was created.", }), ), - id: Schema.String, + id: Schema.String.annotate({ + description: "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + }), modelProvider: Schema.String.annotate({ description: "Model provider used for this thread (for example, 'openai').", }), @@ -37144,6 +41496,15 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ preview: Schema.String.annotate({ description: "Usually the first user message in the thread, if available.", }), + recencyAt: Schema.optionalKey( + Schema.Union([ + Schema.Number.annotate({ + description: "Unix timestamp (in seconds) used for thread recency ordering.", + format: "int64", + }).check(Schema.isInt()), + Schema.Null, + ]), + ), sessionId: Schema.String.annotate({ description: "Session id shared by threads that belong to the same session tree.", }), @@ -37192,6 +41553,14 @@ export const V2ThreadRollbackResponse__Thread = Schema.Struct({ }).check(Schema.isInt()), }); +export type V2ThreadRollbackResponse__ThreadExtra = {}; +export const V2ThreadRollbackResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadRollbackResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadRollbackResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadRollbackResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37246,6 +41615,21 @@ export const V2ThreadSettingsUpdatedNotification = Schema.Struct({ threadSettings: V2ThreadSettingsUpdatedNotification__ThreadSettings, }).annotate({ title: "ThreadSettingsUpdatedNotification" }); +export type V2ThreadSettingsUpdatedNotification__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadSettingsUpdatedNotification__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadSettingsUpdatedNotification__NetworkAccess = "restricted" | "enabled"; export const V2ThreadSettingsUpdatedNotification__NetworkAccess = Schema.Literals([ "restricted", @@ -37339,6 +41723,17 @@ export const V2ThreadStartedNotification__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartedNotification__ThreadExtra = {}; +export const V2ThreadStartedNotification__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadStartedNotification__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartedNotification__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadStartedNotification__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37423,6 +41818,12 @@ export const V2ThreadStartParams = Schema.Struct({ ), }).annotate({ title: "ThreadStartParams" }); +export type V2ThreadStartParams__AbsolutePathBuf = string; +export const V2ThreadStartParams__AbsolutePathBuf = Schema.String.annotate({ + description: + "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", +}); + export type V2ThreadStartParams__CapabilityRootLocation = { readonly environmentId: string; readonly path: string; @@ -37432,7 +41833,9 @@ export const V2ThreadStartParams__CapabilityRootLocation = Schema.Union( [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -37444,19 +41847,52 @@ export const V2ThreadStartParams__CapabilityRootLocation = Schema.Union( { mode: "oneOf" }, ).annotate({ description: "Location used to resolve a selected capability root." }); -export type V2ThreadStartParams__DynamicToolSpec = { - readonly deferLoading?: boolean; - readonly description: string; - readonly inputSchema: unknown; - readonly name: string; - readonly namespace?: string | null; -}; -export const V2ThreadStartParams__DynamicToolSpec = Schema.Struct({ - deferLoading: Schema.optionalKey(Schema.Boolean), - description: Schema.String, - inputSchema: Schema.Unknown, - name: Schema.String, - namespace: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), +export type V2ThreadStartParams__DynamicToolSpec = + | { + readonly deferLoading?: boolean; + readonly description: string; + readonly inputSchema: unknown; + readonly name: string; + readonly type: "function"; + } + | { + readonly description: string; + readonly name: string; + readonly tools: ReadonlyArray; + readonly type: "namespace"; + }; +export const V2ThreadStartParams__DynamicToolSpec = Schema.Union( + [ + Schema.Struct({ + deferLoading: Schema.optionalKey(Schema.Boolean), + description: Schema.String, + inputSchema: Schema.Unknown, + name: Schema.String, + type: Schema.Literal("function").annotate({ title: "FunctionDynamicToolSpecType" }), + }).annotate({ title: "FunctionDynamicToolSpec" }), + Schema.Struct({ + description: Schema.String, + name: Schema.String, + tools: Schema.Array(V2ThreadStartParams__DynamicToolNamespaceTool), + type: Schema.Literal("namespace").annotate({ title: "NamespaceDynamicToolSpecType" }), + }).annotate({ title: "NamespaceDynamicToolSpec" }), + ], + { mode: "oneOf" }, +); + +export type V2ThreadStartParams__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadStartParams__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", }); export type V2ThreadStartParams__SelectedCapabilityRoot = { @@ -37475,7 +41911,9 @@ export const V2ThreadStartParams__SelectedCapabilityRoot = Schema.Struct({ [ Schema.Struct({ environmentId: Schema.String, - path: Schema.String, + path: Schema.String.annotate({ + description: "Absolute path for the root in the selected environment.", + }), type: Schema.Literal("environment").annotate({ title: "EnvironmentCapabilityRootLocationType", }), @@ -37490,20 +41928,32 @@ export const V2ThreadStartParams__SelectedCapabilityRoot = Schema.Struct({ description: "A user-selected root that can expose one or more runtime capabilities.", }); +export type V2ThreadStartParams__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartParams__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadStartParams__TurnEnvironmentParams = { - readonly cwd: V2ThreadStartParams__AbsolutePathBuf; + readonly cwd: V2ThreadStartParams__LegacyAppPathString; readonly environmentId: string; + readonly runtimeWorkspaceRoots?: ReadonlyArray | null; }; export const V2ThreadStartParams__TurnEnvironmentParams = Schema.Struct({ - cwd: V2ThreadStartParams__AbsolutePathBuf, + cwd: V2ThreadStartParams__LegacyAppPathString, environmentId: Schema.String, + runtimeWorkspaceRoots: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2ThreadStartParams__LegacyAppPathString).annotate({ + description: "Environment-native runtime workspace roots. Omitted defaults to `cwd`.", + }), + Schema.Null, + ]), + ), }); export type V2ThreadStartResponse = { readonly approvalPolicy: V2ThreadStartResponse__AskForApproval; readonly approvalsReviewer: "user" | "auto_review" | "guardian_subagent"; readonly cwd: V2ThreadStartResponse__AbsolutePathBuf; - readonly instructionSources?: ReadonlyArray; + readonly instructionSources?: ReadonlyArray; readonly model: string; readonly modelProvider: string; readonly reasoningEffort?: V2ThreadStartResponse__ReasoningEffort | null; @@ -37529,8 +41979,9 @@ export const V2ThreadStartResponse = Schema.Struct({ }), cwd: V2ThreadStartResponse__AbsolutePathBuf, instructionSources: Schema.optionalKey( - Schema.Array(V2ThreadStartResponse__AbsolutePathBuf).annotate({ - description: "Instruction source files currently loaded for this thread.", + Schema.Array(V2ThreadStartResponse__LegacyAppPathString).annotate({ + description: + "Environment-native paths to instruction source files currently loaded for this thread.", default: [], }), ), @@ -37655,6 +42106,21 @@ export const V2ThreadStartResponse__CommandExecutionSource = Schema.Literals([ "unifiedExecInteraction", ]); +export type V2ThreadStartResponse__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2ThreadStartResponse__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2ThreadStartResponse__NetworkAccess = "restricted" | "enabled"; export const V2ThreadStartResponse__NetworkAccess = Schema.Literals(["restricted", "enabled"]); @@ -37720,6 +42186,14 @@ export const V2ThreadStartResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadStartResponse__ThreadExtra = {}; +export const V2ThreadStartResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadStartResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadStartResponse__ThreadHistoryMode = Schema.Literals(["legacy", "paginated"]); + export type V2ThreadStartResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -37854,6 +42328,17 @@ export const V2ThreadUnarchiveResponse__SessionSource = Schema.Union( { mode: "oneOf" }, ); +export type V2ThreadUnarchiveResponse__ThreadExtra = {}; +export const V2ThreadUnarchiveResponse__ThreadExtra = Schema.Struct({}).annotate({ + description: "Extra app-server data for a thread.", +}); + +export type V2ThreadUnarchiveResponse__ThreadHistoryMode = "legacy" | "paginated"; +export const V2ThreadUnarchiveResponse__ThreadHistoryMode = Schema.Literals([ + "legacy", + "paginated", +]); + export type V2ThreadUnarchiveResponse__ThreadStatus = | { readonly type: "notLoaded" } | { readonly type: "idle" } @@ -38187,16 +42672,40 @@ export const V2TurnStartParams__CollaborationMode = Schema.Struct({ settings: V2TurnStartParams__Settings, }).annotate({ description: "Collaboration mode for a Codex session." }); +export type V2TurnStartParams__MultiAgentMode = + | "explicitRequestOnly" + | "proactive" + | { readonly custom: string }; +export const V2TurnStartParams__MultiAgentMode = Schema.Union( + [ + Schema.Literals(["explicitRequestOnly", "proactive"]), + Schema.Struct({ custom: Schema.String }).annotate({ title: "CustomMultiAgentMode" }), + ], + { mode: "oneOf" }, +).annotate({ + description: + "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", +}); + export type V2TurnStartParams__NetworkAccess = "restricted" | "enabled"; export const V2TurnStartParams__NetworkAccess = Schema.Literals(["restricted", "enabled"]); export type V2TurnStartParams__TurnEnvironmentParams = { - readonly cwd: V2TurnStartParams__AbsolutePathBuf; + readonly cwd: V2TurnStartParams__LegacyAppPathString; readonly environmentId: string; + readonly runtimeWorkspaceRoots?: ReadonlyArray | null; }; export const V2TurnStartParams__TurnEnvironmentParams = Schema.Struct({ - cwd: V2TurnStartParams__AbsolutePathBuf, + cwd: V2TurnStartParams__LegacyAppPathString, environmentId: Schema.String, + runtimeWorkspaceRoots: Schema.optionalKey( + Schema.Union([ + Schema.Array(V2TurnStartParams__LegacyAppPathString).annotate({ + description: "Environment-native runtime workspace roots. Omitted defaults to `cwd`.", + }), + Schema.Null, + ]), + ), }); export type V2TurnStartResponse = { readonly turn: V2TurnStartResponse__Turn }; diff --git a/packages/effect-codex-app-server/src/protocol.test.ts b/packages/effect-codex-app-server/src/protocol.test.ts index 0ed81b3b9e6..a68abd537a8 100644 --- a/packages/effect-codex-app-server/src/protocol.test.ts +++ b/packages/effect-codex-app-server/src/protocol.test.ts @@ -23,6 +23,15 @@ const decodeJson = Schema.decodeEffect(Schema.UnknownFromJsonString); const decodeAccountTokenUsageResponse = Schema.decodeUnknownEffect( CodexRpc.CLIENT_REQUEST_RESPONSES["account/usage/read"], ); +const decodeAccountRateLimitsResponse = Schema.decodeUnknownEffect( + CodexRpc.CLIENT_REQUEST_RESPONSES["account/rateLimits/read"], +); +const decodeConsumeRateLimitResetCreditParams = Schema.decodeUnknownEffect( + CodexRpc.CLIENT_REQUEST_PARAMS["account/rateLimitResetCredit/consume"], +); +const decodeConsumeRateLimitResetCreditResponse = Schema.decodeUnknownEffect( + CodexRpc.CLIENT_REQUEST_RESPONSES["account/rateLimitResetCredit/consume"], +); it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => { it.effect("maps account usage responses to the upstream token usage schema", () => @@ -42,6 +51,83 @@ it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => { }), ); + it.effect("maps earned rate-limit reset credits from account rate-limit snapshots", () => + Effect.gen(function* () { + assert.strictEqual( + CodexRpc.CLIENT_REQUEST_RESPONSES["account/rateLimits/read"], + CodexSchema.V2GetAccountRateLimitsResponse, + ); + + const response = { + rateLimits: {}, + rateLimitResetCredits: { + availableCount: 2, + credits: [ + { + id: "RateLimitResetCredit_1", + resetType: "codexRateLimits", + status: "available", + grantedAt: 1_781_654_400, + expiresAt: 1_784_246_400, + title: "Full reset", + description: "Ready to redeem", + }, + { + id: "RateLimitResetCredit_2", + resetType: "unknown", + status: "unknown", + grantedAt: 1_781_654_401, + expiresAt: null, + }, + ], + }, + } as const; + + assert.deepEqual(yield* decodeAccountRateLimitsResponse(response), response); + assert.deepEqual( + yield* decodeAccountRateLimitsResponse({ + rateLimits: {}, + rateLimitResetCredits: { availableCount: 2, credits: null }, + }), + { + rateLimits: {}, + rateLimitResetCredits: { availableCount: 2, credits: null }, + }, + ); + }), + ); + + it.effect("maps the earned rate-limit reset consume request and response", () => + Effect.gen(function* () { + assert.equal( + CodexRpc.CLIENT_REQUEST_METHODS["account/rateLimitResetCredit/consume"], + "account/rateLimitResetCredit/consume", + ); + assert.strictEqual( + CodexRpc.CLIENT_REQUEST_PARAMS["account/rateLimitResetCredit/consume"], + CodexSchema.V2ConsumeAccountRateLimitResetCreditParams, + ); + assert.strictEqual( + CodexRpc.CLIENT_REQUEST_RESPONSES["account/rateLimitResetCredit/consume"], + CodexSchema.V2ConsumeAccountRateLimitResetCreditResponse, + ); + + assert.deepEqual( + yield* decodeConsumeRateLimitResetCreditParams({ + idempotencyKey: "8ae96ff3-3425-4f4c-8772-b6fd61502868", + creditId: "RateLimitResetCredit_1", + }), + { + idempotencyKey: "8ae96ff3-3425-4f4c-8772-b6fd61502868", + creditId: "RateLimitResetCredit_1", + }, + ); + assert.deepEqual(yield* decodeConsumeRateLimitResetCreditResponse({ outcome: "reset" }), { + outcome: "reset", + }); + }), + ); + it.effect( "encodes requests without a jsonrpc field and routes inbound requests and notifications", () => From 2fae0d0ac300353be774c9f9fe79d3af025d4f91 Mon Sep 17 00:00:00 2001 From: Chris Michael Guzman <67719167+Chrrxs@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:49:22 -0400 Subject: [PATCH 29/39] fix(preview): preserve direct localhost navigation (#3939) Co-authored-by: Julius Marminge Co-authored-by: codex --- .../components/preview/PreviewView.test.tsx | 194 ++++++++++++++++++ .../src/components/preview/PreviewView.tsx | 48 +++-- 2 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/components/preview/PreviewView.test.tsx diff --git a/apps/web/src/components/preview/PreviewView.test.tsx b/apps/web/src/components/preview/PreviewView.test.tsx new file mode 100644 index 00000000000..c0d503c7599 --- /dev/null +++ b/apps/web/src/components/preview/PreviewView.test.tsx @@ -0,0 +1,194 @@ +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; + +const mocks = vi.hoisted(() => ({ + navigate: vi.fn(async (_tabId: string, _url: string): Promise => undefined), + rememberPreviewUrl: vi.fn(), + readPreparedConnection: vi.fn(() => ({ httpBaseUrl: "http://172.25.85.75:3773" })), + submittedUrl: null as ((url: string) => void) | null, + emptyStateUrl: null as ((url: string) => void) | null, + showEmptyState: false, +})); + +vi.mock("~/state/session", () => ({ + readPreparedConnection: mocks.readPreparedConnection, +})); + +vi.mock("~/composerDraftStore", () => ({ + useComposerDraftStore: ( + select: (store: { addPreviewAnnotation: () => void; addImage: () => void }) => unknown, + ) => select({ addPreviewAnnotation: vi.fn(), addImage: vi.fn() }), +})); + +vi.mock("~/lib/previewAnnotation", () => ({ + previewAnnotationScreenshotFile: vi.fn(), +})); + +vi.mock("~/localApi", () => ({ + ensureLocalApi: vi.fn(), +})); + +vi.mock("~/previewStateStore", () => ({ + rememberPreviewUrl: mocks.rememberPreviewUrl, + updatePreviewServerSnapshot: vi.fn(), + useThreadPreviewState: () => ({ + activeTabId: "tab-1", + desktopByTabId: { + "tab-1": { + canGoBack: false, + canGoForward: false, + loading: false, + zoomFactor: 1, + controller: "none", + }, + }, + recentlySeenUrls: [], + sessions: mocks.showEmptyState + ? {} + : { + "tab-1": { + threadId: "thread-1", + tabId: "tab-1", + navStatus: { + _tag: "Success", + url: "http://example.com/", + title: "Example", + }, + canGoBack: false, + canGoForward: false, + updatedAt: "2026-07-13T00:00:00.000Z", + }, + }, + }), +})); + +vi.mock("~/state/environments", () => ({ + useEnvironment: () => ({ label: "WSL" }), + useEnvironmentHttpBaseUrl: () => "http://172.25.85.75:3773", +})); + +vi.mock("~/state/preview", () => ({ + previewEnvironment: { open: {}, resize: {} }, +})); + +vi.mock("~/state/use-atom-command", () => ({ + useAtomCommand: () => vi.fn(), +})); + +vi.mock("~/browser/browserRecording", () => ({ + startBrowserRecording: vi.fn(), + stopBrowserRecording: vi.fn(), + useActiveBrowserRecordingTabId: () => null, +})); + +vi.mock("~/browser/browserSurfaceStore", () => ({ + useBrowserSurfaceStore: ( + select: (state: { byTabId: Record }) => unknown, + ) => select({ byTabId: {} }), +})); + +vi.mock("~/components/ui/toast", () => ({ + stackedThreadToast: vi.fn(), + toastManager: { add: vi.fn() }, +})); + +vi.mock("./previewBridge", () => ({ + previewBridge: { navigate: mocks.navigate }, +})); + +vi.mock("./PreviewChromeRow", () => ({ + PreviewChromeRow: (props: { onSubmit: (url: string) => void }) => { + mocks.submittedUrl = props.onSubmit; + return null; + }, +})); + +vi.mock("./PreviewEmptyState", () => ({ + PreviewEmptyState: (props: { onOpenUrl: (url: string) => void }) => { + mocks.emptyStateUrl = props.onOpenUrl; + return null; + }, +})); +vi.mock("./PreviewMoreMenu", () => ({ PreviewMoreMenu: () => null })); +vi.mock("./PreviewUnreachable", () => ({ PreviewUnreachable: () => null })); +vi.mock("./ZoomIndicator", () => ({ ZoomIndicator: () => null })); +vi.mock("./AgentBrowserCursor", () => ({ AgentBrowserCursor: () => null })); +vi.mock("~/browser/BrowserSurfaceSlot", () => ({ BrowserSurfaceSlot: () => null })); +vi.mock("./useLoadingProgress", () => ({ useLoadingProgress: () => 0 })); +vi.mock("./usePreviewSession", () => ({ usePreviewSession: vi.fn() })); + +import { PreviewView } from "./PreviewView"; + +describe("PreviewView navigation", () => { + beforeEach(() => { + mocks.navigate.mockClear(); + mocks.rememberPreviewUrl.mockClear(); + mocks.readPreparedConnection.mockClear(); + mocks.submittedUrl = null; + mocks.emptyStateUrl = null; + mocks.showEmptyState = false; + }); + + it.each([ + [ + "https://localhost:8000/dashboard?mode=test#top", + "https://localhost:8000/dashboard?mode=test#top", + ], + ["localhost:5173/app", "http://localhost:5173/app"], + ])("preserves a direct localhost URL in a WSL environment", async (submitted, expected) => { + renderToStaticMarkup( + , + ); + + expect(mocks.submittedUrl).not.toBeNull(); + mocks.submittedUrl?.(submitted); + + await vi.waitFor(() => expect(mocks.navigate).toHaveBeenCalledWith("tab-1", expected)); + expect(mocks.rememberPreviewUrl).toHaveBeenCalledWith( + { + environmentId: "environment-1", + threadId: "thread-1", + }, + expected, + ); + }); + + it("maps an empty-state localhost server onto the WSL host", async () => { + mocks.showEmptyState = true; + renderToStaticMarkup( + , + ); + + expect(mocks.emptyStateUrl).not.toBeNull(); + mocks.emptyStateUrl?.("http://localhost:5173/app?mode=test#top"); + + await vi.waitFor(() => + expect(mocks.navigate).toHaveBeenCalledWith( + "tab-1", + "http://172.25.85.75:5173/app?mode=test#top", + ), + ); + expect(mocks.rememberPreviewUrl).toHaveBeenCalledWith( + { + environmentId: "environment-1", + threadId: "thread-1", + }, + "http://172.25.85.75:5173/app?mode=test#top", + ); + }); +}); diff --git a/apps/web/src/components/preview/PreviewView.tsx b/apps/web/src/components/preview/PreviewView.tsx index bb1c4d409eb..5f7deeb0fcd 100644 --- a/apps/web/src/components/preview/PreviewView.tsx +++ b/apps/web/src/components/preview/PreviewView.tsx @@ -7,6 +7,7 @@ import { type PreviewViewportSetting, type ScopedThreadRef, } from "@t3tools/contracts"; +import { normalizePreviewUrl } from "@t3tools/shared/preview"; import { useCallback, useEffect, useRef, useState } from "react"; import { useComposerDraftStore } from "~/composerDraftStore"; @@ -112,27 +113,44 @@ export function PreviewView({ threadRef, tabId: requestedTabId, configuredUrls, tabId ? (state.byTabId[tabId]?.rect ?? null) : null, ); + const navigateToResolvedUrl = useCallback( + async (resolvedUrl: string) => { + if (tabId && previewBridge) { + // Drive the webview imperatively; `usePreviewBridge` mirrors the + // resolved URL back to the server so other clients stay in sync. + await previewBridge.navigate(tabId, resolvedUrl); + rememberPreviewUrl(threadRef, resolvedUrl); + } else { + await openPreviewSession({ + openPreview: open, + threadRef, + url: resolvedUrl, + }); + } + }, + [open, tabId, threadRef], + ); + const handleSubmitUrl = useCallback( async (next: string) => { try { - const resolvedUrl = resolveDiscoveredServerUrl(threadRef.environmentId, next); - if (tabId && previewBridge) { - // Drive the webview imperatively; `usePreviewBridge` mirrors the - // resolved URL back to the server so other clients stay in sync. - await previewBridge.navigate(tabId, resolvedUrl); - rememberPreviewUrl(threadRef, resolvedUrl); - } else { - await openPreviewSession({ - openPreview: open, - threadRef, - url: resolvedUrl, - }); - } + await navigateToResolvedUrl(normalizePreviewUrl(next)); } catch { // Server-side `failed` event renders the unreachable view. } }, - [open, tabId, threadRef], + [navigateToResolvedUrl], + ); + + const handleOpenServerUrl = useCallback( + async (next: string) => { + try { + await navigateToResolvedUrl(resolveDiscoveredServerUrl(threadRef.environmentId, next)); + } catch { + // Server-side `failed` event renders the unreachable view. + } + }, + [navigateToResolvedUrl, threadRef.environmentId], ); const handleRefresh = useCallback(() => { @@ -613,7 +631,7 @@ export function PreviewView({ threadRef, tabId: requestedTabId, configuredUrls, environmentId={threadRef.environmentId} configuredUrls={configuredUrls} recentlySeenUrls={previewState.recentlySeenUrls} - onOpenUrl={(next) => void handleSubmitUrl(next)} + onOpenUrl={(next) => void handleOpenServerUrl(next)} /> ) : null} {snapshot && desktopOverlay ? ( From 8e3467fe60c49ee739b278229156108032b72e25 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 20 Jul 2026 11:54:59 +0200 Subject: [PATCH 30/39] Synchronize mobile threads with authoritative shell snapshots (#4163) Co-authored-by: codex --- apps/mobile/src/Stack.tsx | 4 +- .../src/components/CompactBrandTitle.tsx | 60 ------ .../src/features/home/HomeRouteScreen.tsx | 5 +- apps/mobile/src/features/home/HomeScreen.tsx | 11 +- .../home/WorkspaceConnectionStatus.test.ts | 17 ++ .../home/WorkspaceConnectionStatus.tsx | 7 +- .../home/workspace-connection-status.ts | 4 + .../threads/sidebar-navigation-shell.tsx | 4 +- apps/server/src/server.test.ts | 109 +++++++++++ apps/server/src/ws.ts | 54 +++++- packages/client-runtime/src/rpc/client.ts | 140 ++++++++------ .../src/state/shell-sync.test.ts | 142 +++++++++++++- packages/client-runtime/src/state/shell.ts | 113 +++++++---- .../src/state/threads-sync.test.ts | 175 +++++++++++++++++- packages/client-runtime/src/state/threads.ts | 147 ++++++++++----- packages/contracts/src/orchestration.ts | 16 ++ packages/contracts/src/server.ts | 4 + 17 files changed, 779 insertions(+), 233 deletions(-) delete mode 100644 apps/mobile/src/components/CompactBrandTitle.tsx diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index db244d7c726..4eb4a5061e8 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -15,7 +15,6 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re import { useResolveClassNames } from "uniwind"; import { AppText as Text } from "./components/AppText"; -import { renderCompactBrandTitle } from "./components/CompactBrandTitle"; import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent"; @@ -378,8 +377,7 @@ export const RootStack = createNativeStackNavigator({ ...GLASS_HEADER_OPTIONS, contentStyle: { backgroundColor: "transparent" }, headerBackVisible: false, - headerTitle: renderCompactBrandTitle, - title: "T3 Code", + title: "Threads", }, }), Thread: createNativeStackScreen({ diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx deleted file mode 100644 index 2ecf34aa40c..00000000000 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { View } from "react-native"; - -import { AppText as Text } from "./AppText"; -import { T3Wordmark } from "./T3Wordmark"; -import { useThemeColor } from "../lib/useThemeColor"; - -/** - * Compact brand lockup sized for native navigation bars. - */ -export function CompactBrandTitle() { - const iconColor = useThemeColor("--color-icon"); - const mutedColor = useThemeColor("--color-foreground-muted"); - const subtleColor = useThemeColor("--color-subtle"); - - return ( - - - - Code - - - - Alpha - - - - ); -} - -export function renderCompactBrandTitle() { - return ; -} diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index cd9467c774e..49cf06d85ec 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -4,7 +4,6 @@ import { useNavigation } from "@react-navigation/native"; import { useMemo, useState } from "react"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; -import { renderCompactBrandTitle } from "../../components/CompactBrandTitle"; import { useProjects, useThreadShells } from "../../state/entities"; import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; import { useWorkspaceState } from "../../state/workspace"; @@ -87,9 +86,7 @@ export function HomeRouteScreen() { > <> {/* Restore the compact title in case the split branch blanked it. */} - + - {emptyState.loading ? ( + {emptyState.loading && !shouldShowConnectionStatus ? ( ) : null} + {shouldShowConnectionStatus && Platform.OS === "ios" ? ( + + + + ) : null} {connectionStatus} diff --git a/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts index 767f0e3dd3f..8c3c873cc9e 100644 --- a/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts +++ b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts @@ -67,4 +67,21 @@ describe("workspace connection status", () => { expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); expect(workspaceConnectionStatusLabel(state)).toBe("Could not reach Julius’s Mac mini"); }); + + it("shows shell catch-up while cached threads remain visible", () => { + const state = workspaceState({ hasPendingShellSnapshot: true }); + + expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); + expect(workspaceConnectionStatusLabel(state)).toBe("Syncing threads..."); + }); + + it("distinguishes initial shell loading from cached catch-up", () => { + const state = workspaceState({ + hasLoadedShellSnapshot: false, + hasPendingShellSnapshot: true, + }); + + expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); + expect(workspaceConnectionStatusLabel(state)).toBe("Loading threads..."); + }); }); diff --git a/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx b/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx index 8acdacfbbb3..1e986ad1a50 100644 --- a/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx +++ b/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx @@ -12,7 +12,10 @@ export function WorkspaceConnectionStatus(props: { readonly variant?: "floating" | "sidebar"; }) { const iconColor = useThemeColor("--color-icon-muted"); - const isReconnecting = props.state.connectingEnvironments.length > 0; + const isSynchronizing = + props.state.networkStatus !== "offline" && + props.state.connectionError === null && + (props.state.connectingEnvironments.length > 0 || props.state.hasPendingShellSnapshot); const variant = props.variant ?? "floating"; return ( @@ -37,7 +40,7 @@ export function WorkspaceConnectionStatus(props: { : undefined } > - {isReconnecting ? ( + {isSynchronizing ? ( ) : ( diff --git a/apps/mobile/src/features/home/workspace-connection-status.ts b/apps/mobile/src/features/home/workspace-connection-status.ts index 7d2c6d840d4..d8eed4383b1 100644 --- a/apps/mobile/src/features/home/workspace-connection-status.ts +++ b/apps/mobile/src/features/home/workspace-connection-status.ts @@ -5,6 +5,7 @@ export function shouldShowWorkspaceConnectionStatus(state: WorkspaceState): bool state.networkStatus === "offline" || state.connectionError !== null || state.hasConnectingEnvironment || + state.hasPendingShellSnapshot || (state.hasLoadedShellSnapshot && !state.hasReadyEnvironment) ); } @@ -18,5 +19,8 @@ export function workspaceConnectionStatusLabel(state: WorkspaceState): string { return `Reconnecting ${state.connectingEnvironments.length} environments`; } if (state.connectionError !== null) return state.connectionError; + if (state.hasPendingShellSnapshot) { + return state.hasLoadedShellSnapshot ? "Syncing threads..." : "Loading threads..."; + } return "Not connected"; } diff --git a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx index a94c033fa4d..f1e06926d0b 100644 --- a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx +++ b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx @@ -11,7 +11,6 @@ import { import type { ReactNode } from "react"; import { Platform, useColorScheme } from "react-native"; -import { renderCompactBrandTitle } from "../../components/CompactBrandTitle"; import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); @@ -35,11 +34,10 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { headerShadowVisible: false, headerShown: true, headerStyle: { backgroundColor: "transparent" }, - headerTitle: renderCompactBrandTitle, headerTitleStyle: { fontSize: 18, fontWeight: "800" }, headerTransparent: true, scrollEdgeEffects: SCROLL_EDGE_EFFECTS, - title: "T3 Code", + title: "Threads", unstable_navigationItemStyle: "editor", }; diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index b1f6c1f7251..a0f41cbf0fc 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -3717,6 +3717,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => { assert.equal(response.environment.environmentId, testEnvironmentDescriptor.environmentId); assert.equal(response.auth.policy, "desktop-managed-local"); + assert.equal(response.shellResumeCompletionMarker, true); + assert.equal(response.threadResumeCompletionMarker, true); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); @@ -5607,6 +5609,113 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("marks an empty shell catch-up replay as synchronized when requested", () => + Effect.gen(function* () { + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + readEvents: () => Stream.empty, + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const firstItem = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ + afterSequence: 0, + requestCompletionMarker: true, + }).pipe(Stream.runHead), + ), + ); + + assert.deepEqual(Option.getOrThrow(firstItem), { kind: "synchronized" }); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("marks a socket thread snapshot as synchronized when requested", () => + Effect.gen(function* () { + const thread = makeDefaultOrchestrationReadModel().threads[0]!; + yield* buildAppUnderTest({ + layers: { + projectionSnapshotQuery: { + getThreadDetailSnapshot: () => + Effect.succeed(Option.some({ snapshotSequence: 1, thread })), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeThread]({ + threadId: defaultThreadId, + requestCompletionMarker: true, + }).pipe(Stream.take(2), Stream.runCollect), + ), + ); + + assert.equal(items[0]?.kind, "snapshot"); + assert.deepEqual(items[1], { kind: "synchronized" }); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("buffers shell events published while the fallback snapshot loads", () => + Effect.gen(function* () { + const liveEvents = yield* PubSub.unbounded(); + const deletedEvent = { + sequence: 2, + eventId: EventId.make("event-shell-thread-deleted"), + aggregateKind: "thread", + aggregateId: defaultThreadId, + occurredAt: "2026-01-01T00:00:01.000Z", + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.deleted", + payload: { + threadId: defaultThreadId, + deletedAt: "2026-01-01T00:00:01.000Z", + }, + } satisfies Extract; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + streamDomainEvents: Stream.fromPubSub(liveEvents), + }, + projectionSnapshotQuery: { + getShellSnapshot: () => + Effect.gen(function* () { + yield* Effect.sleep("25 millis"); + yield* PubSub.publish(liveEvents, deletedEvent); + return { + snapshotSequence: 1, + projects: [], + threads: [makeDefaultOrchestrationThreadShell()], + updatedAt: "2026-01-01T00:00:00.000Z", + }; + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ + requestCompletionMarker: true, + }).pipe(Stream.take(3), Stream.runCollect), + ), + ).pipe(Effect.timeout("2 seconds")); + + assert.equal(items[0]?.kind, "snapshot"); + assert.equal(items[1]?.kind, "thread-removed"); + assert.deepEqual(items[2], { kind: "synchronized" }); + }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), + ); + it.effect("buffers thread events published while the initial snapshot loads", () => Effect.gen(function* () { const thread = makeDefaultOrchestrationReadModel().threads[0]!; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index c7223d09dc5..41c29fc3388 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -936,6 +936,8 @@ const makeWsRpcLayer = ( otlpMetricsEnabled: config.otlpMetricsUrl !== undefined, }, settings, + shellResumeCompletionMarker: true, + threadResumeCompletionMarker: true, }; }); @@ -1105,11 +1107,28 @@ const makeWsRpcLayer = ( }), ), ); - return Stream.concat(catchUpStream, Stream.fromQueue(liveBuffer)); + const afterCatchUp = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + Stream.fromQueue(liveBuffer), + ) + : Stream.fromQueue(liveBuffer); + return Stream.concat(catchUpStream, afterCatchUp); }), ); } + // The full-snapshot fallback needs the same replay-window safety + // as the resume path: subscribe before loading the projection so + // events published while the snapshot is read are buffered. + const liveBuffer = yield* Queue.unbounded(); + yield* Effect.forkScoped( + liveStream.pipe(Stream.runForEach((item) => Queue.offer(liveBuffer, item))), + ); + const bufferedLiveStream = Stream.fromQueue(liveBuffer); const snapshot = yield* projectionSnapshotQuery.getShellSnapshot().pipe( Effect.tapError((cause) => Effect.logError("orchestration shell snapshot load failed", { cause }), @@ -1123,12 +1142,21 @@ const makeWsRpcLayer = ( ), ); + const afterSnapshot = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; return Stream.concat( Stream.make({ kind: "snapshot" as const, snapshot, }), - liveStream, + afterSnapshot, ); }), { "rpc.aggregate": "orchestration" }, @@ -1207,7 +1235,16 @@ const makeWsRpcLayer = ( }), ), ); - return Stream.concat(catchUpStream, bufferedLiveStream); + const afterCatchUp = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; + return Stream.concat(catchUpStream, afterCatchUp); } const snapshot = yield* projectionSnapshotQuery @@ -1229,12 +1266,21 @@ const makeWsRpcLayer = ( }); } + const afterSnapshot = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; return Stream.concat( Stream.make({ kind: "snapshot" as const, snapshot: snapshot.value, }), - bufferedLiveStream, + afterSnapshot, ); }), { "rpc.aggregate": "orchestration" }, diff --git a/packages/client-runtime/src/rpc/client.ts b/packages/client-runtime/src/rpc/client.ts index 92892431e45..c7c928b3c95 100644 --- a/packages/client-runtime/src/rpc/client.ts +++ b/packages/client-runtime/src/rpc/client.ts @@ -11,6 +11,7 @@ import { RpcClientError } from "effect/unstable/rpc"; import { EnvironmentSupervisor } from "../connection/supervisor.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; +import type { RpcSession } from "../rpc/session.ts"; export class EnvironmentRpcUnavailableError extends Schema.TaggedErrorClass()( "EnvironmentRpcUnavailableError", @@ -147,15 +148,18 @@ export function runStream( ); } -export function subscribe( +interface SubscriptionOptions { + readonly onExpectedFailure?: ( + cause: Cause.Cause>, + ) => Effect.Effect; + readonly retryExpectedFailureAfter?: Duration.Input; + readonly resubscribe?: Stream.Stream; +} + +export function subscribeDynamic( tag: TTag, - input: EnvironmentRpcInput, - options?: { - readonly onExpectedFailure?: ( - cause: Cause.Cause>, - ) => Effect.Effect; - readonly retryExpectedFailureAfter?: Duration.Input; - }, + makeInput: (session: RpcSession) => Effect.Effect>, + options?: SubscriptionOptions, ): Stream.Stream< EnvironmentRpcStreamValue, EnvironmentRpcStreamFailure, @@ -163,8 +167,18 @@ export function subscribe( > { return Stream.unwrap( EnvironmentSupervisor.pipe( - Effect.map((supervisor) => - SubscriptionRef.changes(supervisor.session).pipe( + Effect.map((supervisor) => { + const sessionChanges = SubscriptionRef.changes(supervisor.session); + const sessions = + options?.resubscribe === undefined + ? sessionChanges + : Stream.merge( + sessionChanges, + options.resubscribe.pipe( + Stream.mapEffect(() => SubscriptionRef.get(supervisor.session)), + ), + ); + return sessions.pipe( Stream.switchMap( Option.match({ onNone: () => Stream.empty, @@ -180,54 +194,64 @@ export function subscribe( EnvironmentRpcStreamFailure > => Stream.suspend(() => - method(input).pipe( - Stream.catchCause((cause) => { - const hasOnlyExpectedFailures = - cause.reasons.length > 0 && - cause.reasons.every((reason) => reason._tag === "Fail"); - const isTransportFailure = - hasOnlyExpectedFailures && - cause.reasons.every( - (reason) => reason._tag === "Fail" && isRpcClientError(reason.error), - ); - if (isTransportFailure) { - return Stream.fromEffect( - Effect.logWarning( - "Durable RPC subscription lost its transport; waiting for the next session.", - { - cause: Cause.pretty(cause), - method: tag, - environmentId: supervisor.target.environmentId, - }, - ), - ).pipe(Stream.drain); - } - if (hasOnlyExpectedFailures && options?.onExpectedFailure !== undefined) { - const handled = Stream.fromEffect(options.onExpectedFailure(cause)).pipe( - Stream.drain, - ); - if (options.retryExpectedFailureAfter === undefined) { - return handled; - } - return handled.pipe( - Stream.concat( - Stream.fromEffect( - Effect.sleep(options.retryExpectedFailureAfter), - ).pipe(Stream.drain), - ), - Stream.concat(subscribeToSession()), - ); - } - return Stream.failCause(cause); - }), + Stream.unwrap( + makeInput(session).pipe( + Effect.map((input) => + method(input).pipe( + Stream.catchCause((cause) => { + const hasOnlyExpectedFailures = + cause.reasons.length > 0 && + cause.reasons.every((reason) => reason._tag === "Fail"); + const isTransportFailure = + hasOnlyExpectedFailures && + cause.reasons.every( + (reason) => + reason._tag === "Fail" && isRpcClientError(reason.error), + ); + if (isTransportFailure) { + return Stream.fromEffect( + Effect.logWarning( + "Durable RPC subscription lost its transport; waiting for the next session.", + { + cause: Cause.pretty(cause), + method: tag, + environmentId: supervisor.target.environmentId, + }, + ), + ).pipe(Stream.drain); + } + if ( + hasOnlyExpectedFailures && + options?.onExpectedFailure !== undefined + ) { + const handled = Stream.fromEffect( + options.onExpectedFailure(cause), + ).pipe(Stream.drain); + if (options.retryExpectedFailureAfter === undefined) { + return handled; + } + return handled.pipe( + Stream.concat( + Stream.fromEffect( + Effect.sleep(options.retryExpectedFailureAfter), + ).pipe(Stream.drain), + ), + Stream.concat(subscribeToSession()), + ); + } + return Stream.failCause(cause); + }), + ), + ), + ), ), ); return subscribeToSession(); }, }), ), - ), - ), + ); + }), ), ).pipe( Stream.withSpan("EnvironmentRpc.subscribe", { @@ -236,6 +260,18 @@ export function subscribe( ); } +export function subscribe( + tag: TTag, + input: EnvironmentRpcInput, + options?: SubscriptionOptions, +): Stream.Stream< + EnvironmentRpcStreamValue, + EnvironmentRpcStreamFailure, + EnvironmentSupervisor +> { + return subscribeDynamic(tag, () => Effect.succeed(input), options); +} + export const config = Effect.gen(function* () { const session = yield* currentSession(); return yield* session.initialConfig; diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index b6eb4e9f710..a4514d5f0e6 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -8,6 +8,7 @@ import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; @@ -17,6 +18,7 @@ import { type PreparedConnection, } from "../connection/model.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as ConnectionWakeups from "../connection/wakeups.ts"; import * as Persistence from "../platform/persistence.ts"; import * as RpcSession from "../rpc/session.ts"; import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; @@ -48,7 +50,7 @@ const LIVE_SHELL_SNAPSHOT: OrchestrationShellSnapshot = { function session(client: WsRpcProtocolClient): RpcSession.RpcSession { return { client, - initialConfig: Effect.never, + initialConfig: Effect.succeed({ shellResumeCompletionMarker: true } as never), ready: Effect.void, probe: Effect.void, closed: Effect.never, @@ -112,6 +114,15 @@ describe("environment shell synchronization", () => { kind: "snapshot", snapshot: LIVE_SHELL_SNAPSHOT, }); + const synchronizing = yield* SubscriptionRef.changes(shellState).pipe( + Stream.filter((state) => state.status === "synchronizing" && Option.isSome(state.snapshot)), + Stream.runHead, + ); + expect(Option.getOrThrow(Option.getOrThrow(synchronizing).snapshot)).toEqual( + LIVE_SHELL_SNAPSHOT, + ); + + yield* Queue.offer(events, { kind: "synchronized" }); yield* SubscriptionRef.changes(shellState).pipe( Stream.filter((state) => state.status === "live"), Stream.runHead, @@ -137,21 +148,32 @@ describe("environment shell synchronization", () => { }), ); - it.effect("resumes a warm shell cache via afterSequence without an HTTP fetch", () => + it.effect("replaces a warm shell cache with an authoritative HTTP snapshot", () => Effect.gen(function* () { const cachedSnapshot: OrchestrationShellSnapshot = { snapshotSequence: 5, projects: [], - threads: [], + threads: [{ id: "stale-thread" } as never], updatedAt: "2026-06-06T00:00:00.000Z", }; + const httpSnapshot: OrchestrationShellSnapshot = { + ...cachedSnapshot, + snapshotSequence: 9, + threads: [], + updatedAt: "2026-06-07T00:00:00.000Z", + }; const events = yield* Queue.unbounded(); const capturedAfterSequence = yield* SubscriptionRef.make(undefined); + const capturedCompletionMarker = yield* Ref.make(undefined); const loaderCalls = yield* SubscriptionRef.make(0); const client = { - [ORCHESTRATION_WS_METHODS.subscribeShell]: (input: { readonly afterSequence?: number }) => + [ORCHESTRATION_WS_METHODS.subscribeShell]: (input: { + readonly afterSequence?: number; + readonly requestCompletionMarker?: boolean; + }) => Stream.unwrap( - SubscriptionRef.set(capturedAfterSequence, input.afterSequence).pipe( + Ref.set(capturedCompletionMarker, input.requestCompletionMarker).pipe( + Effect.andThen(SubscriptionRef.set(capturedAfterSequence, input.afterSequence)), Effect.as(Stream.fromQueue(events)), ), ), @@ -183,9 +205,11 @@ describe("environment shell synchronization", () => { }); const snapshotLoader = ShellSnapshotLoader.of({ load: () => - SubscriptionRef.update(loaderCalls, (count) => count + 1).pipe(Effect.as(Option.none())), + SubscriptionRef.update(loaderCalls, (count) => count + 1).pipe( + Effect.as(Option.some(httpSnapshot)), + ), }); - yield* makeEnvironmentShellState().pipe( + const shellState = yield* makeEnvironmentShellState().pipe( Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), Effect.provideService(ShellSnapshotLoader, snapshotLoader), @@ -197,8 +221,108 @@ describe("environment shell synchronization", () => { Stream.runHead, ); - expect(yield* SubscriptionRef.get(capturedAfterSequence)).toBe(5); - expect(yield* SubscriptionRef.get(loaderCalls)).toBe(0); + expect(yield* SubscriptionRef.get(capturedAfterSequence)).toBe(9); + expect(yield* Ref.get(capturedCompletionMarker)).toBe(true); + expect(yield* SubscriptionRef.get(loaderCalls)).toBe(1); + const synchronizing = yield* SubscriptionRef.get(shellState); + expect(synchronizing.status).toBe("synchronizing"); + expect(Option.getOrThrow(synchronizing.snapshot)).toEqual(httpSnapshot); + + yield* Queue.offer(events, { kind: "synchronized" }); + yield* SubscriptionRef.changes(shellState).pipe( + Stream.filter((value) => value.status === "live"), + Stream.runHead, + ); + }), + ); + + it.effect("refreshes the authoritative shell snapshot when the app becomes active", () => + Effect.gen(function* () { + const events = yield* Queue.unbounded(); + const wakeups = yield* Queue.unbounded(); + const loaderCalls = yield* Ref.make(0); + const subscriptionCount = yield* Ref.make(0); + const client = { + [ORCHESTRATION_WS_METHODS.subscribeShell]: () => + Stream.unwrap( + Ref.update(subscriptionCount, (count) => count + 1).pipe( + Effect.as(Stream.fromQueue(events)), + ), + ), + } as unknown as WsRpcProtocolClient; + const supervisorState = yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE); + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ + target: TARGET, + state: supervisorState, + session: yield* SubscriptionRef.make(Option.some(session(client))), + prepared: yield* SubscriptionRef.make(Option.some(PREPARED)), + connect: Effect.void, + disconnect: Effect.void, + retryNow: Effect.void, + } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); + const cache = Persistence.EnvironmentCacheStore.of({ + loadShell: () => Effect.succeed(Option.some(LIVE_SHELL_SNAPSHOT)), + saveShell: () => Effect.void, + loadThread: () => Effect.succeed(Option.none()), + saveThread: () => Effect.void, + removeThread: () => Effect.void, + loadServerConfig: () => Effect.succeed(Option.none()), + saveServerConfig: () => Effect.void, + loadVcsRefs: () => Effect.succeed(Option.none()), + saveVcsRefs: () => Effect.void, + clear: () => Effect.void, + }); + const snapshotLoader = ShellSnapshotLoader.of({ + load: () => + Ref.updateAndGet(loaderCalls, (count) => count + 1).pipe( + Effect.map((count) => + Option.some({ ...LIVE_SHELL_SNAPSHOT, snapshotSequence: count * 10 }), + ), + ), + }); + const shellState = yield* makeEnvironmentShellState().pipe( + Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + Effect.provideService(Persistence.EnvironmentCacheStore, cache), + Effect.provideService(ShellSnapshotLoader, snapshotLoader), + Effect.provideService( + ConnectionWakeups.ConnectionWakeups, + ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), + ), + ); + + yield* SubscriptionRef.changes(shellState).pipe( + Stream.filter( + (value) => + value.status === "synchronizing" && + Option.isSome(value.snapshot) && + value.snapshot.value.snapshotSequence === 10, + ), + Stream.runHead, + ); + yield* Queue.offer(events, { kind: "synchronized" }); + yield* SubscriptionRef.changes(shellState).pipe( + Stream.filter((value) => value.status === "live"), + Stream.runHead, + ); + + yield* Queue.offer(wakeups, "application-active"); + yield* SubscriptionRef.changes(shellState).pipe( + Stream.filter( + (value) => + value.status === "synchronizing" && + Option.isSome(value.snapshot) && + value.snapshot.value.snapshotSequence === 20, + ), + Stream.runHead, + ); + + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(subscriptionCount)) >= 2) break; + yield* Effect.yieldNow; + } + + expect(yield* Ref.get(loaderCalls)).toBe(2); + expect(yield* Ref.get(subscriptionCount)).toBe(2); }), ); }); diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index faa70bc4f3a..6ccb11797f5 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -9,6 +9,7 @@ import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; @@ -16,9 +17,10 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { EnvironmentRegistry } from "../connection/registry.ts"; import { connectionProjectionPhase } from "../connection/model.ts"; import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as ConnectionWakeups from "../connection/wakeups.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; import { EnvironmentCacheStore } from "../platform/persistence.ts"; -import { subscribe } from "../rpc/client.ts"; +import { subscribeDynamic } from "../rpc/client.ts"; import { ShellSnapshotLoader } from "./shellSnapshotHttp.ts"; import { applyShellStreamEvent } from "./shellReducer.ts"; import type { EnvironmentCatalogState } from "./connections.ts"; @@ -50,6 +52,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") const supervisor = yield* EnvironmentSupervisor; const cache = yield* EnvironmentCacheStore; const snapshotLoader = yield* ShellSnapshotLoader; + const wakeups = yield* Effect.serviceOption(ConnectionWakeups.ConnectionWakeups); const environmentId = supervisor.target.environmentId; const cachedSnapshot = yield* cache.loadShell(environmentId).pipe( Effect.catch((error) => @@ -67,6 +70,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") status: shellStatusForSnapshot(cachedSnapshot), error: Option.none(), }); + const awaitingCompletion = yield* Ref.make(false); const persistence = yield* Queue.sliding(1); const persist = Effect.fn("EnvironmentShellState.persist")(function* ( @@ -90,10 +94,14 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") Effect.forkScoped, ); - const setDisconnected = SubscriptionRef.update(state, (current) => ({ - ...current, - status: shellStatusForSnapshot(current.snapshot), - })); + const setDisconnected = Ref.set(awaitingCompletion, false).pipe( + Effect.andThen( + SubscriptionRef.update(state, (current) => ({ + ...current, + status: shellStatusForSnapshot(current.snapshot), + })), + ), + ); const setSynchronizing = SubscriptionRef.update(state, (current) => ({ ...current, status: "synchronizing" as const, @@ -109,7 +117,8 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") }, ); const setStreamError = (error: unknown) => - Effect.logWarning("Could not synchronize the environment shell.").pipe( + Ref.set(awaitingCompletion, false).pipe( + Effect.andThen(Effect.logWarning("Could not synchronize the environment shell.")), Effect.annotateLogs({ environmentId, ...safeErrorLogAttributes(error), @@ -126,6 +135,16 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") const applyItem = Effect.fn("EnvironmentShellState.applyItem")(function* ( item: OrchestrationShellStreamItem, ) { + if (item.kind === "synchronized") { + yield* Ref.set(awaitingCompletion, false); + yield* SubscriptionRef.update(state, (current) => + Option.isSome(current.snapshot) + ? { ...current, status: "live" as const, error: Option.none() } + : current, + ); + return; + } + const current = yield* SubscriptionRef.get(state); const nextSnapshot = item.kind === "snapshot" @@ -141,52 +160,64 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") return; } + const waiting = yield* Ref.get(awaitingCompletion); yield* SubscriptionRef.set(state, { snapshot: Option.some(nextSnapshot), - status: "live", + status: waiting ? "synchronizing" : "live", error: Option.none(), }); yield* Queue.offer(persistence, nextSnapshot); }); - yield* Effect.forkScoped( - Effect.gen(function* () { - // Establish the base shell snapshot to resume from, minimizing bytes over - // the wire: - // - Warm cache: reuse the cached snapshot (zero network) and resume via - // `afterSequence` so we only receive shell events since the cached - // sequence. - // - Cold cache: load the full shell snapshot over HTTP (gzip-compressible, - // and off the socket), then resume via `afterSequence`. - // If no base can be established we fall back to the socket-embedded - // snapshot so the shell still synchronizes. Overlapping/replayed events are - // deduped by sequence in applyItem. - const base = Option.isSome(cachedSnapshot) - ? cachedSnapshot - : yield* Effect.gen(function* () { - const prepared = yield* SubscriptionRef.changes(supervisor.prepared).pipe( - Stream.filter(Option.isSome), - Stream.map((current) => current.value), - Stream.runHead, - ); - return Option.isSome(prepared) - ? yield* snapshotLoader.load(prepared.value) - : Option.none(); - }); + const foregroundResubscriptions = Option.match(wakeups, { + onNone: () => Stream.never, + onSome: (service) => + service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + }); - if (Option.isSome(base)) { - yield* applyItem({ kind: "snapshot", snapshot: base.value }); - } + yield* setSynchronizing; + yield* Effect.forkScoped( + subscribeDynamic( + ORCHESTRATION_WS_METHODS.subscribeShell, + Effect.fn("EnvironmentShellState.makeSubscribeInput")(function* (session) { + const supportsCompletionMarker = yield* session.initialConfig.pipe( + Effect.map((config) => config.shellResumeCompletionMarker === true), + Effect.orElseSucceed(() => false), + ); + yield* Ref.set(awaitingCompletion, supportsCompletionMarker); + yield* setSynchronizing; - const subscribeInput = Option.match(base, { - onNone: () => ({}), - onSome: (snapshot) => ({ afterSequence: snapshot.snapshotSequence }), - }); + const prepared = yield* SubscriptionRef.get(supervisor.prepared).pipe( + Effect.flatMap( + Option.match({ + onSome: Effect.succeed, + onNone: () => + SubscriptionRef.changes(supervisor.prepared).pipe( + Stream.filter(Option.isSome), + Stream.map((value) => value.value), + Stream.runHead, + Effect.map(Option.getOrThrow), + ), + }), + ), + ); + const httpSnapshot = yield* snapshotLoader.load(prepared); + if (Option.isSome(httpSnapshot)) { + yield* applyItem({ kind: "snapshot", snapshot: httpSnapshot.value }); + return { + afterSequence: httpSnapshot.value.snapshotSequence, + ...(supportsCompletionMarker ? { requestCompletionMarker: true as const } : {}), + }; + } - yield* subscribe(ORCHESTRATION_WS_METHODS.subscribeShell, subscribeInput, { + return supportsCompletionMarker ? { requestCompletionMarker: true as const } : {}; + }), + { onExpectedFailure: (cause) => setStreamError(Cause.squash(cause)), - }).pipe(Stream.runForEach(applyItem)); - }), + retryExpectedFailureAfter: "250 millis", + resubscribe: foregroundResubscriptions, + }, + ).pipe(Stream.runForEach(applyItem)), ); yield* SubscriptionRef.changes(supervisor.state).pipe( Stream.runForEach((connectionState) => { diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 630a5f27d16..f3c4c4e6338 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -26,6 +26,7 @@ import { type PreparedConnection, type SupervisorConnectionState, } from "../connection/model.ts"; +import * as ConnectionWakeups from "../connection/wakeups.ts"; import * as EnvironmentSupervisor from "../connection/supervisor.ts"; import * as Persistence from "../platform/persistence.ts"; import * as RpcSession from "../rpc/session.ts"; @@ -98,10 +99,17 @@ const ACTIVE_THREAD: OrchestrationThread = { type TestThreadInput = OrchestrationThreadStreamItem | Error; -function testSession(client: WsRpcProtocolClient): RpcSession.RpcSession { +function testSession( + client: WsRpcProtocolClient, + options?: { readonly completionMarker?: boolean }, +): RpcSession.RpcSession { return { client, - initialConfig: Effect.never, + initialConfig: Effect.succeed( + options?.completionMarker === true + ? ({ threadResumeCompletionMarker: true } as never) + : ({} as never), + ), ready: Effect.void, probe: Effect.void, closed: Effect.never, @@ -122,6 +130,7 @@ function awaitThreadState( const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (options?: { readonly cached?: OrchestrationThread; readonly httpSnapshot?: Option.Option; + readonly completionMarker?: boolean; }) { const inputs = yield* Queue.unbounded(); const observed = yield* Queue.unbounded(); @@ -130,8 +139,10 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o const subscriptionCount = yield* Ref.make(0); const loaderCalls = yield* Ref.make(0); const lastSubscribeAfterSequence = yield* Ref.make(undefined); + const lastRequestCompletionMarker = yield* Ref.make(undefined); const savedThreads = yield* Ref.make>([]); const removedThreads = yield* Ref.make>([]); + const wakeups = yield* Queue.unbounded(); const supervisorState = yield* SubscriptionRef.make( AVAILABLE_CONNECTION_STATE, ); @@ -142,16 +153,25 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o ), ); const client = { - [ORCHESTRATION_WS_METHODS.subscribeThread]: (input: { readonly afterSequence?: number }) => + [ORCHESTRATION_WS_METHODS.subscribeThread]: (input: { + readonly afterSequence?: number; + readonly requestCompletionMarker?: boolean; + }) => Stream.unwrap( Ref.updateAndGet(subscriptionCount, (count) => count + 1).pipe( Effect.andThen(Ref.set(lastSubscribeAfterSequence, input.afterSequence)), + Effect.andThen(Ref.set(lastRequestCompletionMarker, input.requestCompletionMarker)), Effect.as(streamFrom(inputs)), ), ), } as unknown as WsRpcProtocolClient; const supervisorSession = yield* SubscriptionRef.make>( - Option.some(testSession(client)), + Option.some( + testSession( + client, + options?.completionMarker === true ? { completionMarker: true } : undefined, + ), + ), ); const prepared = yield* SubscriptionRef.make>( Option.some(PREPARED), @@ -201,6 +221,10 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), Effect.provideService(Persistence.EnvironmentCacheStore, cache), Effect.provideService(ThreadSnapshotLoader, snapshotLoader), + Effect.provideService( + ConnectionWakeups.ConnectionWakeups, + ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.fromQueue(wakeups) }), + ), ); yield* SubscriptionRef.changes(threadState).pipe( Stream.runForEach((state) => @@ -217,11 +241,21 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o subscriptionCount, loaderCalls, lastSubscribeAfterSequence, + lastRequestCompletionMarker, supervisorState, supervisorSession, savedThreads, removedThreads, - replaceSession: SubscriptionRef.set(supervisorSession, Option.some(testSession(client))), + wakeups, + replaceSession: SubscriptionRef.set( + supervisorSession, + Option.some( + testSession( + client, + options?.completionMarker === true ? { completionMarker: true } : undefined, + ), + ), + ), }; }); @@ -233,6 +267,8 @@ const snapshot = (thread: OrchestrationThread): OrchestrationThreadStreamItem => }, }); +const synchronized = (): OrchestrationThreadStreamItem => ({ kind: "synchronized" }); + const titleUpdated = (title: string, sequence = 2): OrchestrationThreadStreamItem => ({ kind: "event", event: { @@ -415,6 +451,35 @@ describe("EnvironmentThreads", () => { }), ); + it.effect("does not resurrect a deleted thread when the app returns to the foreground", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + cached: BASE_THREAD, + completionMarker: true, + httpSnapshot: Option.some({ + snapshotSequence: 4, + thread: { ...BASE_THREAD, title: "Stale HTTP thread" }, + }), + }); + yield* Queue.offer(harness.inputs, snapshot(BASE_THREAD)); + yield* Queue.offer(harness.inputs, deleted()); + yield* awaitThreadState(harness.observed, (value) => value.status === "deleted"); + + expect(yield* Ref.get(harness.loaderCalls)).toBe(0); + yield* Queue.offer(harness.wakeups, "application-active"); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(harness.subscriptionCount)) >= 2) break; + yield* Effect.yieldNow; + } + + const latest = yield* Ref.get(harness.latest); + expect(yield* Ref.get(harness.subscriptionCount)).toBe(2); + expect(yield* Ref.get(harness.loaderCalls)).toBe(0); + expect(latest.status).toBe("deleted"); + expect(Option.isNone(latest.data)).toBe(true); + }), + ); + it.effect("preserves data after a domain failure and resumes on a replacement session", () => Effect.gen(function* () { const harness = yield* makeHarness({ cached: BASE_THREAD }); @@ -529,4 +594,104 @@ describe("EnvironmentThreads", () => { expect((yield* Ref.get(harness.latest)).status).toBe("live"); }), ); + + it.effect("keeps replayed updates synchronizing until the completion marker arrives", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ cached: BASE_THREAD, completionMarker: true }); + yield* awaitThreadState( + harness.observed, + (value) => value.status === "synchronizing" && Option.isSome(value.data), + ); + expect(yield* Ref.get(harness.lastRequestCompletionMarker)).toBe(true); + + yield* Queue.offer( + harness.inputs, + titleUpdated("Caught-up title", CACHED_SNAPSHOT_SEQUENCE + 1), + ); + const catchingUp = yield* awaitThreadState( + harness.observed, + (value) => + value.status === "synchronizing" && + Option.isSome(value.data) && + value.data.value.title === "Caught-up title", + ); + expect(catchingUp.status).toBe("synchronizing"); + + yield* Queue.offer(harness.inputs, synchronized()); + const live = yield* awaitThreadState( + harness.observed, + (value) => value.status === "live" && Option.isSome(value.data), + ); + expect(Option.getOrThrow(live.data).title).toBe("Caught-up title"); + }), + ); + + it.effect("resumes replacement sessions from the latest applied sequence", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ cached: BASE_THREAD, completionMarker: true }); + yield* Queue.offer( + harness.inputs, + titleUpdated("Latest title", CACHED_SNAPSHOT_SEQUENCE + 1), + ); + yield* Queue.offer(harness.inputs, synchronized()); + yield* awaitThreadState( + harness.observed, + (value) => + value.status === "live" && + Option.isSome(value.data) && + value.data.value.title === "Latest title", + ); + + yield* harness.replaceSession; + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(harness.subscriptionCount)) >= 2) break; + yield* Effect.yieldNow; + } + + expect(yield* Ref.get(harness.subscriptionCount)).toBe(2); + expect(yield* Ref.get(harness.lastSubscribeAfterSequence)).toBe(CACHED_SNAPSHOT_SEQUENCE + 1); + expect((yield* Ref.get(harness.latest)).status).toBe("synchronizing"); + }), + ); + + it.effect("resubscribes on app foreground from the latest applied sequence", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ cached: BASE_THREAD, completionMarker: true }); + yield* Queue.offer( + harness.inputs, + titleUpdated("Latest title", CACHED_SNAPSHOT_SEQUENCE + 1), + ); + yield* Queue.offer(harness.inputs, synchronized()); + yield* awaitThreadState( + harness.observed, + (value) => + value.status === "live" && + Option.isSome(value.data) && + value.data.value.title === "Latest title", + ); + + yield* Queue.offer(harness.wakeups, "application-active"); + const synchronizing = yield* awaitThreadState( + harness.observed, + (value) => value.status === "synchronizing" && Option.isSome(value.data), + ); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(harness.subscriptionCount)) >= 2) break; + yield* Effect.yieldNow; + } + + expect(synchronizing.status).toBe("synchronizing"); + expect(yield* Ref.get(harness.subscriptionCount)).toBe(2); + expect(yield* Ref.get(harness.lastSubscribeAfterSequence)).toBe(CACHED_SNAPSHOT_SEQUENCE + 1); + expect(yield* Ref.get(harness.lastRequestCompletionMarker)).toBe(true); + expect(yield* Ref.get(harness.loaderCalls)).toBe(0); + + yield* Queue.offer(harness.inputs, synchronized()); + const live = yield* awaitThreadState( + harness.observed, + (value) => value.status === "live" && Option.isSome(value.data), + ); + expect(Option.getOrThrow(live.data).title).toBe("Latest title"); + }), + ); }); diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 0c9ec340610..196229cc8b1 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -10,6 +10,7 @@ import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; +import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { Atom } from "effect/unstable/reactivity"; @@ -17,8 +18,9 @@ import { Atom } from "effect/unstable/reactivity"; import { EnvironmentRegistry } from "../connection/registry.ts"; import { connectionProjectionPhase } from "../connection/model.ts"; import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import * as ConnectionWakeups from "../connection/wakeups.ts"; import { EnvironmentCacheStore } from "../platform/persistence.ts"; -import { subscribe } from "../rpc/client.ts"; +import { subscribeDynamic } from "../rpc/client.ts"; import { ThreadSnapshotLoader } from "./threadSnapshotHttp.ts"; import { parseThreadKey, threadKey } from "./entities.ts"; import { applyThreadDetailEvent } from "./threadReducer.ts"; @@ -52,6 +54,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make const supervisor = yield* EnvironmentSupervisor; const cache = yield* EnvironmentCacheStore; const snapshotLoader = yield* ThreadSnapshotLoader; + const wakeups = yield* Effect.serviceOption(ConnectionWakeups.ConnectionWakeups); const environmentId = supervisor.target.environmentId; const cached = yield* cache.loadThread(environmentId, threadId).pipe( Effect.catch((error) => @@ -76,6 +79,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make const lastSequence = yield* SubscriptionRef.make( Option.match(cached, { onNone: () => 0, onSome: (snapshot) => snapshot.snapshotSequence }), ); + const awaitingCompletion = yield* Ref.make(false); const persistence = yield* Queue.sliding(1); const persist = Effect.fn("EnvironmentThreadState.persist")(function* ( @@ -100,11 +104,15 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make Effect.forkScoped, ); - const setSynchronizing = SubscriptionRef.update(state, (current) => ({ - ...current, - status: "synchronizing" as const, - error: Option.none(), - })); + const setSynchronizing = SubscriptionRef.update(state, (current) => + current.status === "deleted" + ? current + : { + ...current, + status: "synchronizing" as const, + error: Option.none(), + }, + ); const setReady = SubscriptionRef.update(state, (current) => current.status === "live" || current.status === "deleted" ? current @@ -114,23 +122,32 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make error: Option.none(), }, ); - const setDisconnected = SubscriptionRef.update(state, (current) => ({ - ...current, - status: current.status === "deleted" ? current.status : statusWithoutLiveData(current.data), - })); - const setStreamError = (cause: Cause.Cause) => - SubscriptionRef.update(state, (current) => ({ + const setDisconnected = Effect.gen(function* () { + yield* Ref.set(awaitingCompletion, false); + yield* SubscriptionRef.update(state, (current) => ({ ...current, status: current.status === "deleted" ? current.status : statusWithoutLiveData(current.data), - error: Option.some(formatThreadError(cause)), })); + }); + const setStreamError = (cause: Cause.Cause) => + Ref.set(awaitingCompletion, false).pipe( + Effect.andThen( + SubscriptionRef.update(state, (current) => ({ + ...current, + status: + current.status === "deleted" ? current.status : statusWithoutLiveData(current.data), + error: Option.some(formatThreadError(cause)), + })), + ), + ); const setThread = Effect.fn("EnvironmentThreadState.setThread")(function* ( thread: OrchestrationThread, ) { + const waiting = yield* Ref.get(awaitingCompletion); yield* SubscriptionRef.set(state, { data: Option.some(thread), - status: "live", + status: waiting ? "synchronizing" : "live", error: Option.none(), }); // Active threads can update many times per second and retain large tool @@ -143,6 +160,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make }); const setDeleted = Effect.fn("EnvironmentThreadState.setDeleted")(function* () { + yield* Ref.set(awaitingCompletion, false); yield* SubscriptionRef.set(state, { data: Option.none(), status: "deleted", @@ -164,6 +182,16 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make const applyItem = Effect.fn("EnvironmentThreadState.applyItem")(function* ( item: OrchestrationThreadStreamItem, ) { + if (item.kind === "synchronized") { + yield* Ref.set(awaitingCompletion, false); + yield* SubscriptionRef.update(state, (current) => + Option.isSome(current.data) && current.status !== "deleted" + ? { ...current, status: "live" as const, error: Option.none() } + : current, + ); + return; + } + if (item.kind === "snapshot") { yield* SubscriptionRef.set(lastSequence, item.snapshot.snapshotSequence); yield* setThread(item.snapshot.thread); @@ -205,48 +233,69 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make Effect.forkScoped, ); + const foregroundResubscriptions = Option.match(wakeups, { + onNone: () => Stream.never, + onSome: (service) => + service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + }); + yield* setSynchronizing; yield* Effect.forkScoped( - Effect.gen(function* () { - // Establish the base snapshot to resume from, minimizing bytes over the - // wire: - // - Warm cache: reuse the cached snapshot (zero network) and resume via - // `afterSequence` so we only receive events since the cached sequence. - // - Cold cache: load the full snapshot over HTTP (gzip-compressible, and - // off the socket), then resume via `afterSequence`. - // If no base can be established we fall back to the socket-embedded - // snapshot so the thread still synchronizes. Overlapping/replayed events - // are deduped by sequence in applyItem. - const base = Option.isSome(cached) - ? cached - : yield* Effect.gen(function* () { - // Cold cache only: wait for a prepared connection so we can - // authenticate the HTTP request; this mirrors the socket path, which - // likewise waits for a live session. - const prepared = yield* SubscriptionRef.changes(supervisor.prepared).pipe( - Stream.filter(Option.isSome), - Stream.map((current) => current.value), - Stream.runHead, - ); - return Option.isSome(prepared) - ? yield* snapshotLoader.load(prepared.value, threadId) - : Option.none(); - }); + subscribeDynamic( + ORCHESTRATION_WS_METHODS.subscribeThread, + Effect.fn("EnvironmentThreadState.makeSubscribeInput")(function* (session) { + const supportsCompletionMarker = yield* session.initialConfig.pipe( + Effect.map((config) => config.threadResumeCompletionMarker === true), + Effect.orElseSucceed(() => false), + ); + yield* Ref.set(awaitingCompletion, supportsCompletionMarker); + yield* setSynchronizing; - if (Option.isSome(base)) { - yield* applyItem({ kind: "snapshot", snapshot: base.value }); - } + let current = yield* SubscriptionRef.get(state); + if (Option.isNone(current.data) && current.status !== "deleted") { + const prepared = yield* SubscriptionRef.get(supervisor.prepared).pipe( + Effect.flatMap( + Option.match({ + onSome: Effect.succeed, + onNone: () => + SubscriptionRef.changes(supervisor.prepared).pipe( + Stream.filter(Option.isSome), + Stream.map((value) => value.value), + Stream.runHead, + Effect.map(Option.getOrThrow), + ), + }), + ), + ); + const httpSnapshot = yield* snapshotLoader.load(prepared, threadId); + if (Option.isSome(httpSnapshot)) { + yield* applyItem({ kind: "snapshot", snapshot: httpSnapshot.value }); + current = yield* SubscriptionRef.get(state); + } + } - const subscribeInput = Option.match(base, { - onNone: () => ({ threadId }), - onSome: (snapshot) => ({ threadId, afterSequence: snapshot.snapshotSequence }), - }); + const sequence = yield* SubscriptionRef.get(lastSequence); + const canResume = Option.isSome(current.data); + if (!supportsCompletionMarker && canResume) { + yield* SubscriptionRef.update(state, (value) => ({ + ...value, + status: value.status === "deleted" ? value.status : ("live" as const), + error: Option.none(), + })); + } - yield* subscribe(ORCHESTRATION_WS_METHODS.subscribeThread, subscribeInput, { + return { + threadId, + ...(canResume ? { afterSequence: sequence } : {}), + ...(supportsCompletionMarker ? { requestCompletionMarker: true as const } : {}), + }; + }), + { onExpectedFailure: setStreamError, retryExpectedFailureAfter: "250 millis", - }).pipe(Stream.runForEach(applyItem)); - }), + resubscribe: foregroundResubscriptions, + }, + ).pipe(Stream.runForEach(applyItem)), ); yield* Effect.addFinalizer(() => diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 7e9c421b5df..c0c9c62d080 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -443,6 +443,9 @@ export const OrchestrationShellStreamEvent = Schema.Union([ export type OrchestrationShellStreamEvent = typeof OrchestrationShellStreamEvent.Type; export const OrchestrationShellStreamItem = Schema.Union([ + Schema.Struct({ + kind: Schema.Literal("synchronized"), + }), Schema.Struct({ kind: Schema.Literal("snapshot"), snapshot: OrchestrationShellSnapshot, @@ -461,6 +464,11 @@ export const OrchestrationSubscribeShellInput = Schema.Struct({ * client). */ afterSequence: Schema.optionalKey(NonNegativeInt), + /** + * Requests an explicit marker after the subscription has emitted its initial + * snapshot or catch-up replay and before it begins emitting live events. + */ + requestCompletionMarker: Schema.optionalKey(Schema.Boolean), }); export type OrchestrationSubscribeShellInput = typeof OrchestrationSubscribeShellInput.Type; @@ -474,6 +482,11 @@ export const OrchestrationSubscribeThreadInput = Schema.Struct({ * sequence on the client). */ afterSequence: Schema.optionalKey(NonNegativeInt), + /** + * Requests an explicit marker after the subscription has emitted its initial + * snapshot or catch-up replay and before it begins emitting live events. + */ + requestCompletionMarker: Schema.optionalKey(Schema.Boolean), }); export type OrchestrationSubscribeThreadInput = typeof OrchestrationSubscribeThreadInput.Type; @@ -1135,6 +1148,9 @@ export const OrchestrationEvent = Schema.Union([ export type OrchestrationEvent = typeof OrchestrationEvent.Type; export const OrchestrationThreadStreamItem = Schema.Union([ + Schema.Struct({ + kind: Schema.Literal("synchronized"), + }), Schema.Struct({ kind: Schema.Literal("snapshot"), snapshot: OrchestrationThreadDetailSnapshot, diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index b76ea965afe..316f09693ec 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -417,6 +417,10 @@ export const ServerConfig = Schema.Struct({ availableEditors: Schema.Array(EditorId), observability: ServerObservability, settings: ServerSettings, + /** Whether shell subscriptions can emit an opt-in catch-up completion marker. */ + shellResumeCompletionMarker: Schema.optionalKey(Schema.Boolean), + /** Whether thread subscriptions can emit an opt-in catch-up completion marker. */ + threadResumeCompletionMarker: Schema.optionalKey(Schema.Boolean), }); export type ServerConfig = typeof ServerConfig.Type; From b6d9ce325c4586ea3ab2d2ea5cfd5e8f8c167aeb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 20 Jul 2026 12:17:30 +0200 Subject: [PATCH 31/39] Gate iOS glass layout on native support (#4032) Co-authored-by: codex --- apps/mobile/src/Stack.tsx | 18 ++++++++++++------ .../src/features/files/FileTreeBrowser.tsx | 18 +++++++----------- apps/mobile/src/features/home/HomeScreen.tsx | 7 ++++--- .../features/threads/ThreadDetailScreen.tsx | 9 ++++++++- .../threads/ThreadNavigationSidebar.tsx | 7 +++++-- .../src/features/threads/ThreadRouteScreen.tsx | 3 ++- .../threads/sidebar-navigation-shell.tsx | 9 +++++---- .../src/lib/native-glass-capability.test.ts | 18 ++++++++++++++++++ apps/mobile/src/lib/native-glass-capability.ts | 6 ++++++ apps/mobile/src/native/native-glass.ts | 9 +++++++++ 10 files changed, 76 insertions(+), 28 deletions(-) create mode 100644 apps/mobile/src/lib/native-glass-capability.test.ts create mode 100644 apps/mobile/src/lib/native-glass-capability.ts create mode 100644 apps/mobile/src/native/native-glass.ts diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 4eb4a5061e8..4cf787d9ce5 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -59,6 +59,7 @@ import { EMPTY_INCOMING_SHARE_PRESENTATION_STATE, transitionIncomingSharePresentation, } from "./features/sharing/incoming-share-presentation"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; @@ -78,19 +79,24 @@ type AppScreenOptions = NativeStackNavigationOptions & { // Shared header presets. Screens only override genuinely dynamic values (titles, // subtitles, toolbar items, search callbacks) via NativeStackScreenOptions. // -// GLASS: transparent header over the screen's primary scroll view, with the iOS 26 -// scroll-edge blur sampling the content (Home, Thread, Files tree, settings sheet). +// GLASS: transparent header over the screen's primary scroll view on supported +// iOS versions. Pre-glass iOS gets the same solid material as internal-scroll +// surfaces so content is laid out below the bar instead of underlapping it. const GLASS_HEADER_OPTIONS: AppScreenOptions = { headerBackButtonDisplayMode: "minimal", headerBackTitle: "", headerLargeTitle: false, headerShadowVisible: false, headerShown: true, - headerStyle: Platform.OS === "ios" ? { backgroundColor: "transparent" } : undefined, + headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED + ? { backgroundColor: "transparent" } + : SHEET_BACKGROUND_COLOR !== undefined + ? { backgroundColor: SHEET_BACKGROUND_COLOR as unknown as string } + : undefined, headerTitleStyle: { fontSize: 18, fontWeight: "800" }, - headerTransparent: Platform.OS === "ios", - scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined, - unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, + headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, + scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? HEADER_SCROLL_EDGE_EFFECTS : undefined, + unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, }; // SOLID: opaque sheet-colored header for surfaces whose content scrolls internally diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx index b29121201f5..dd7a1271194 100644 --- a/apps/mobile/src/features/files/FileTreeBrowser.tsx +++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx @@ -1,20 +1,14 @@ import type { ProjectEntry } from "@t3tools/contracts"; import { SymbolView } from "../../components/AppSymbol"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - ActivityIndicator, - FlatList, - Platform, - Pressable, - RefreshControl, - View, -} from "react-native"; +import { ActivityIndicator, FlatList, Pressable, RefreshControl, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; import { PierreEntryIcon } from "../../components/PierreEntryIcon"; import { cn } from "../../lib/cn"; import { useThemeColor } from "../../lib/useThemeColor"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { buildFileTree, defaultExpandedTreePaths, @@ -129,7 +123,7 @@ export function FileTreeBrowser(props: { const insets = useSafeAreaInsets(); // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the // observed adjustedContentInset bottom (~102) seen in the native trace. - const headerInset = Platform.OS === "ios" ? insets.top + 44 : 0; + const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 44 : 0; const iconColor = String(useThemeColor("--color-icon-muted")); const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; const controlledSelectedPathRef = useRef(controlledSelectedPath); @@ -249,9 +243,11 @@ export function FileTreeBrowser(props: { className="flex-1" data={visibleNodes} keyExtractor={(item) => item.node.path} - contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} + contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"} scrollIndicatorInsets={ - Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined + NATIVE_LIQUID_GLASS_SUPPORTED + ? { top: headerInset, left: 0, right: 0, bottom: 0 } + : undefined } keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index c463e4a1bb3..dcc56fd77c5 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -24,6 +24,7 @@ import { EmptyState } from "../../components/EmptyState"; import type { WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; import { scopedProjectKey } from "../../lib/scopedEntities"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { @@ -388,7 +389,7 @@ export function HomeScreen(props: HomeScreenProps) { className="flex-1 items-center justify-center bg-screen px-8" style={{ paddingBottom: Math.max(insets.bottom, 24), - paddingTop: Platform.OS === "ios" ? insets.top + 72 : 0, + paddingTop: NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + 72 : 0, }} > @@ -469,8 +470,8 @@ export function HomeScreen(props: HomeScreenProps) { ListHeaderComponent={listHeader} ListEmptyComponent={listEmpty} style={{ flex: 1 }} - automaticallyAdjustsScrollIndicatorInsets={Platform.OS === "ios"} - contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} + automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED} + contentInsetAdjustmentBehavior={NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never"} showsVerticalScrollIndicator={false} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 31ad5660983..32527b0b719 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -28,6 +28,7 @@ import type { StatusTone } from "../../components/StatusPill"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; import { CHAT_CONTENT_MAX_WIDTH, type LayoutVariant } from "../../lib/layout"; import { scopedThreadKey } from "../../lib/scopedEntities"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import type { PendingApproval, PendingUserInput, @@ -197,7 +198,13 @@ const WorkingDurationPill = memo(function WorkingDurationPill(props: { entering={FadeInDown.duration(200)} exiting={FadeOut.duration(140)} > - + diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 6e0e243aad6..f7ee4eaa74f 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -17,6 +17,7 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { SymbolView } from "../../components/AppSymbol"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { NativeStackScreenOptions } from "../../native/StackHeader"; import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -571,8 +572,10 @@ function ThreadNavigationSidebarPane( itemsAreEqual={homeListItemsAreEqual} keyExtractor={(item) => item.key} renderItem={renderListItem} - automaticallyAdjustsScrollIndicatorInsets - contentInsetAdjustmentBehavior="automatic" + automaticallyAdjustsScrollIndicatorInsets={NATIVE_LIQUID_GLASS_SUPPORTED} + contentInsetAdjustmentBehavior={ + NATIVE_LIQUID_GLASS_SUPPORTED ? "automatic" : "never" + } contentContainerStyle={[ styles.threadListContent, { diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index e60f03bb8c3..7fb4740ddce 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -23,6 +23,7 @@ import { } from "../../components/AndroidScreenHeader"; import { LoadingScreen } from "../../components/LoadingScreen"; import { scopedThreadKey } from "../../lib/scopedEntities"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { connectionTone } from "../connection/connectionTone"; import { @@ -278,7 +279,7 @@ function ThreadRouteContent( ); /* ─── Native header theming ──────────────────────────────────────── */ - const usesNativeHeaderGlass = Platform.OS === "ios"; + const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED; const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, diff --git a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx index f1e06926d0b..f0e3f89c07d 100644 --- a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx +++ b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx @@ -11,6 +11,7 @@ import { import type { ReactNode } from "react"; import { Platform, useColorScheme } from "react-native"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); @@ -33,12 +34,12 @@ const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { headerLargeTitle: false, headerShadowVisible: false, headerShown: true, - headerStyle: { backgroundColor: "transparent" }, + headerStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? { backgroundColor: "transparent" } : undefined, headerTitleStyle: { fontSize: 18, fontWeight: "800" }, - headerTransparent: true, - scrollEdgeEffects: SCROLL_EDGE_EFFECTS, + headerTransparent: NATIVE_LIQUID_GLASS_SUPPORTED, + scrollEdgeEffects: NATIVE_LIQUID_GLASS_SUPPORTED ? SCROLL_EDGE_EFFECTS : undefined, title: "Threads", - unstable_navigationItemStyle: "editor", + unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, }; const SidebarStack = createNativeStackNavigator(); diff --git a/apps/mobile/src/lib/native-glass-capability.test.ts b/apps/mobile/src/lib/native-glass-capability.test.ts new file mode 100644 index 00000000000..43f865c77c3 --- /dev/null +++ b/apps/mobile/src/lib/native-glass-capability.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { supportsNativeLiquidGlass } from "./native-glass-capability"; + +describe("supportsNativeLiquidGlass", () => { + it("uses native liquid glass when iOS reports the capability", () => { + expect(supportsNativeLiquidGlass("ios", true)).toBe(true); + }); + + it("keeps pre-glass iOS on the solid fallback", () => { + expect(supportsNativeLiquidGlass("ios", false)).toBe(false); + }); + + it("does not enable iOS liquid-glass layout behavior on other platforms", () => { + expect(supportsNativeLiquidGlass("android", true)).toBe(false); + expect(supportsNativeLiquidGlass("web", true)).toBe(false); + }); +}); diff --git a/apps/mobile/src/lib/native-glass-capability.ts b/apps/mobile/src/lib/native-glass-capability.ts new file mode 100644 index 00000000000..61c8547b507 --- /dev/null +++ b/apps/mobile/src/lib/native-glass-capability.ts @@ -0,0 +1,6 @@ +export function supportsNativeLiquidGlass( + platform: string, + nativeCapabilityAvailable: boolean, +): boolean { + return platform === "ios" && nativeCapabilityAvailable; +} diff --git a/apps/mobile/src/native/native-glass.ts b/apps/mobile/src/native/native-glass.ts new file mode 100644 index 00000000000..40b28076d36 --- /dev/null +++ b/apps/mobile/src/native/native-glass.ts @@ -0,0 +1,9 @@ +import { isLiquidGlassSupported } from "@callstack/liquid-glass"; +import { Platform } from "react-native"; + +import { supportsNativeLiquidGlass } from "../lib/native-glass-capability"; + +export const NATIVE_LIQUID_GLASS_SUPPORTED = supportsNativeLiquidGlass( + Platform.OS, + isLiquidGlassSupported, +); From f4da4f3b4037260bbb0d8914acbebafd2206607a Mon Sep 17 00:00:00 2001 From: Vadym Kotai Date: Mon, 20 Jul 2026 14:20:28 +0400 Subject: [PATCH 32/39] fix(opencode): resume the OpenCode session on follow-ups instead of starting an empty one (#3617) Co-authored-by: codex --- .../provider/Layers/OpenCodeAdapter.test.ts | 382 +++++++++++++++++- .../src/provider/Layers/OpenCodeAdapter.ts | 242 ++++++++++- 2 files changed, 601 insertions(+), 23 deletions(-) diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts index 4358cf305b0..9e7211dc3e8 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts @@ -5,8 +5,10 @@ import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; +import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; @@ -31,6 +33,8 @@ import { } from "../opencodeRuntime.ts"; import { appendOpenCodeAssistantTextDelta, + isOpenCodeNotFound, + isSameOpenCodeDirectory, makeOpenCodeAdapter, mergeOpenCodeAssistantText, } from "./OpenCodeAdapter.ts"; @@ -64,6 +68,12 @@ const runtimeMock = { closeError: null as Error | null, messages: [] as MessageEntry[], subscribedEvents: [] as unknown[], + sessionGetIds: [] as string[], + missingSessionIds: new Set(), + transientErrorSessionIds: new Set(), + sessionDirectoryById: new Map(), + sessionUpdateCalls: [] as Array<{ sessionID: string; permission: unknown }>, + forkCalls: [] as Array<{ sessionID: string; directory?: string }>, }, reset() { this.state.startCalls.length = 0; @@ -78,6 +88,12 @@ const runtimeMock = { this.state.closeError = null; this.state.messages = []; this.state.subscribedEvents = []; + this.state.sessionGetIds.length = 0; + this.state.missingSessionIds.clear(); + this.state.transientErrorSessionIds.clear(); + this.state.sessionDirectoryById.clear(); + this.state.sessionUpdateCalls.length = 0; + this.state.forkCalls.length = 0; }, }; @@ -102,10 +118,8 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { connectToOpenCodeServer: ({ serverUrl }) => Effect.gen(function* () { const url = serverUrl ?? "http://127.0.0.1:4301"; - // Unconditionally register a scope finalizer for test observability — - // preserves the `closeCalls` / `closeError` probes that the existing - // suites rely on. Production code never attaches a finalizer to an - // external server (it simply returns `Effect.succeed(...)`). + // Always register a finalizer so the closeCalls/closeError probes fire; + // production attaches none for external servers. yield* Effect.addFinalizer(() => Effect.sync(() => { runtimeMock.state.closeCalls.push(url); @@ -132,6 +146,34 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { ); return { data: { id: `${baseUrl}/session` } }; }, + get: async ({ sessionID }: { sessionID: string }) => { + runtimeMock.state.sessionGetIds.push(sessionID); + // The real client is `throwOnError: true`: non-2xx rejects rather + // than resolving, so missing → 404 throw, transient → 500 throw. + if (runtimeMock.state.transientErrorSessionIds.has(sessionID)) { + throw new Error("opencode server error", { cause: { status: 500 } }); + } + if (runtimeMock.state.missingSessionIds.has(sessionID)) { + throw new Error(`Session not found: ${sessionID}`, { + cause: { status: 404, body: { name: "NotFoundError" } }, + }); + } + const directory = runtimeMock.state.sessionDirectoryById.get(sessionID); + return { data: { id: sessionID, ...(directory ? { directory } : {}) } }; + }, + update: async ({ sessionID, permission }: { sessionID: string; permission: unknown }) => { + runtimeMock.state.sessionUpdateCalls.push({ sessionID, permission }); + return { data: { id: sessionID } }; + }, + fork: async ({ sessionID, directory }: { sessionID: string; directory?: string }) => { + // Fork clones history into a new session bound to the directory. + const forkedId = `${sessionID}_fork`; + runtimeMock.state.forkCalls.push({ sessionID, ...(directory ? { directory } : {}) }); + if (directory) { + runtimeMock.state.sessionDirectoryById.set(forkedId, directory); + } + return { data: { id: forkedId, ...(directory ? { directory } : {}) } }; + }, abort: async ({ sessionID }: { sessionID: string }) => { runtimeMock.state.abortCalls.push(sessionID); }, @@ -251,6 +293,256 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { }), ); + it.effect("returns a durable resume cursor for a freshly created session", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-cursor"); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + }); + + // Without a persisted cursor, a session is created and its id is + // surfaced as a resume cursor so the upper layer can persist it. + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "http://127.0.0.1:9999/session", + }); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("resumes the persisted OpenCode session instead of creating a new one", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-resume"); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, + }); + + // The adapter validates the persisted id with session.get and re-adopts + // it — no new session is minted (issue #3604). + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_persisted"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "ses_persisted", + }); + // Resume re-asserts the permission ruleset for the current runtimeMode. + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_persisted"); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("sends follow-up turns to the resumed session id", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-resume-turn"); + + yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_persisted" }, + }); + + const result = yield* adapter.sendTurn({ + threadId, + input: "continue where we left off", + modelSelection: createModelSelection( + ProviderInstanceId.make("opencode"), + "anthropic/sonnet", + ), + }); + + // The prompt targets the resumed id, and the turn re-surfaces the cursor. + NodeAssert.deepEqual( + (runtimeMock.state.promptCalls[0] as { sessionID: string }).sessionID, + "ses_persisted", + ); + NodeAssert.deepEqual(result.resumeCursor, { + schemaVersion: 1, + sessionId: "ses_persisted", + }); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("falls back to a fresh session when the persisted session is gone", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-stale"); + runtimeMock.state.missingSessionIds.add("ses_stale"); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_stale" }, + }); + + // get probed the stale id, found nothing, then created a new session and + // emitted a fresh cursor rather than wedging the thread. + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_stale"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "http://127.0.0.1:9999/session", + }); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("ignores a malformed or wrong-version resume cursor", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-badcursor"); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 99, sessionId: "ses_persisted" }, + }); + + // A foreign/stale-shaped cursor is treated as "no resume": never probed, + // a fresh session is created. + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, []); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, ["http://127.0.0.1:9999"]); + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "http://127.0.0.1:9999/session", + }); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("surfaces a non-not-found resume probe error instead of silently starting fresh", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-transient"); + // session.get returns a 500 (not a 404) for this id. + runtimeMock.state.transientErrorSessionIds.add("ses_transient"); + + const exit = yield* Effect.exit( + adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_transient" }, + }), + ); + + // A transient/transport/auth failure must propagate — NOT be masked as a + // brand-new empty session (the #3604 class of silent context loss). + NodeAssert.equal(Exit.isFailure(exit), true); + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_transient"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); + }), + ); + + it.effect("re-applies the current runtimeMode permissions when resuming", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-perms"); + + yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + // A different runtimeMode than the original create — resume must not + // leave the upstream session on stale permissions. + runtimeMode: "approval-required", + threadId, + resumeCursor: { schemaVersion: 1, sessionId: "ses_perms" }, + }); + + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_perms"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_perms"); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.permission != null, true); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect( + "forks the resumed session into the requested directory instead of losing context", + () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-cwd"); + // The persisted session still exists but was created in another working dir + // (e.g. the thread moved from the project root into a git worktree). + runtimeMock.state.sessionDirectoryById.set("ses_otherdir", "/some/other/worktree"); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_otherdir" }, + }); + + // A cwd change must not mint an empty session: the adapter forks the + // persisted session into the requested cwd, carrying history forward. + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_otherdir"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); + NodeAssert.equal(runtimeMock.state.forkCalls.length, 1); + NodeAssert.equal(runtimeMock.state.forkCalls[0]?.sessionID, "ses_otherdir"); + NodeAssert.equal(typeof runtimeMock.state.forkCalls[0]?.directory, "string"); + // Permission ruleset re-asserted on the fork for the current runtimeMode. + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls.length, 1); + NodeAssert.equal(runtimeMock.state.sessionUpdateCalls[0]?.sessionID, "ses_otherdir_fork"); + // Durable cursor now points at the history-complete fork in the new directory. + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "ses_otherdir_fork", + }); + + yield* adapter.stopSession(threadId); + }), + ); + + it.effect("reuses the resumed session when the stored directory differs only lexically", () => + Effect.gen(function* () { + const adapter = yield* OpenCodeAdapter; + const threadId = asThreadId("thread-opencode-samedir"); + // Same working tree, different spelling (trailing slash) — must reuse, + // not fork. + runtimeMock.state.sessionDirectoryById.set("ses_samedir", `${process.cwd()}/`); + + const session = yield* adapter.startSession({ + provider: ProviderDriverKind.make("opencode"), + threadId, + runtimeMode: "full-access", + resumeCursor: { schemaVersion: 1, sessionId: "ses_samedir" }, + }); + + NodeAssert.deepEqual(runtimeMock.state.sessionGetIds, ["ses_samedir"]); + NodeAssert.deepEqual(runtimeMock.state.sessionCreateUrls, []); + NodeAssert.deepEqual(runtimeMock.state.forkCalls, []); + NodeAssert.deepEqual(session.resumeCursor, { + schemaVersion: 1, + sessionId: "ses_samedir", + }); + + yield* adapter.stopSession(threadId); + }), + ); + it.effect("fails sendTurn for missing sessions through the typed error channel", () => Effect.gen(function* () { const adapter = yield* OpenCodeAdapter; @@ -673,6 +965,88 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { }), ); + it.effect("classifies a confirmed not-found across the shapes the SDK/runtime can produce", () => + Effect.sync(() => { + // The real production shape: runOpenCodeSdk wraps the thrown Error + // (cause = { body, status }) under OpenCodeRuntimeError. + const wrappedError = new Error("Session not found: ses_x", { + cause: { body: { name: "NotFoundError" }, status: 404 }, + }); + NodeAssert.equal( + isOpenCodeNotFound({ + _tag: "OpenCodeRuntimeError", + operation: "session.get", + detail: "Session not found: ses_x", + cause: wrappedError, + }), + true, + ); + + // 404 expressed only via response.status (the bot's flagged shape). + NodeAssert.equal(isOpenCodeNotFound({ cause: { response: { status: 404 } } }), true); + // 404 via a bare numeric status / statusCode. + NodeAssert.equal(isOpenCodeNotFound(new Error("x", { cause: { status: 404 } })), true); + NodeAssert.equal(isOpenCodeNotFound({ statusCode: 404 }), true); + // OpenCode NotFoundError body name with no status. + NodeAssert.equal(isOpenCodeNotFound({ body: { name: "NotFoundError" } }), true); + + // NOT a miss: only structured signals count, never free text. A non-404 + // error whose message/detail merely contains "not found" must propagate, + // not be misread as a missing session and silently start fresh. + NodeAssert.equal( + isOpenCodeNotFound(new Error("upstream provider not found", { cause: { status: 500 } })), + false, + ); + NodeAssert.equal(isOpenCodeNotFound({ detail: "status=500 body={...not found...}" }), false); + // An explicit non-404 status seals its subtree: a 500 whose serialized + // body echoes a NotFoundError name — or that is itself named + // *NotFound* — is a real failure, never a miss. + NodeAssert.equal(isOpenCodeNotFound({ status: 500, body: { name: "NotFoundError" } }), false); + NodeAssert.equal(isOpenCodeNotFound({ name: "UpstreamNotFoundError", status: 500 }), false); + // A "NotFound"-flavored name that isn't OpenCode's exact `NotFoundError` + // is not a confirmed miss even without a sealing status. + NodeAssert.equal(isOpenCodeNotFound({ name: "UpstreamNotFoundError" }), false); + NodeAssert.equal(isOpenCodeNotFound({ cause: { name: "ProviderNotFoundError" } }), false); + NodeAssert.equal( + isOpenCodeNotFound( + new Error("x", { cause: { status: 502, body: { name: "NotFoundError" } } }), + ), + false, + ); + // Other transient/auth/network failures must propagate too. + NodeAssert.equal(isOpenCodeNotFound(new Error("boom", { cause: { status: 500 } })), false); + NodeAssert.equal(isOpenCodeNotFound({ cause: { response: { status: 401 } } }), false); + NodeAssert.equal(isOpenCodeNotFound(new Error("network error (no response)")), false); + NodeAssert.equal(isOpenCodeNotFound(undefined), false); + }), + ); + + it.effect("treats lexically or physically identical directories as the same", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const sameDirectory = (left: string, right: string) => + isSameOpenCodeDirectory(fileSystem, path, left, right); + + // Lexical-only differences (trailing slash, dot segments) short-circuit + // without touching the filesystem — the paths need not exist. + NodeAssert.equal(yield* sameDirectory("/repo/project/", "/repo/project"), true); + NodeAssert.equal(yield* sameDirectory("/repo/nested/../project", "/repo/project"), true); + // Nonexistent paths degrade to the lexical comparison instead of failing. + NodeAssert.equal(yield* sameDirectory("/repo/project", "/repo/other"), false); + + // A symlinked cwd (the macOS `/tmp` → `/private/tmp` shape) resolves to + // the directory it points at, so the two spellings compare equal. + const base = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-opencode-dir-" }); + const real = path.join(base, "real"); + const link = path.join(base, "link"); + yield* fileSystem.makeDirectory(real); + yield* fileSystem.symlink(real, link); + NodeAssert.equal(yield* sameDirectory(link, real), true); + NodeAssert.equal(yield* sameDirectory(link, path.join(base, "other")), false); + }).pipe(Effect.scoped), + ); + it.effect("appends raw assistant text deltas and reconciles part update snapshots", () => Effect.sync(() => { const firstUpdate = mergeOpenCodeAssistantText(undefined, "Hello"); diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts index e7622e6c705..73c23b77e68 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts @@ -17,6 +17,8 @@ import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Scope from "effect/Scope"; @@ -53,6 +55,114 @@ import * as Option from "effect/Option"; const PROVIDER = ProviderDriverKind.make("opencode"); +/** + * Version tag stamped into the OpenCode resume cursor. Bump if the cursor + * shape changes so stale-shaped cursors written by older builds are ignored + * rather than misread (mirrors GROK_RESUME_VERSION / CURSOR_RESUME_VERSION). + */ +const OPENCODE_RESUME_VERSION = 1 as const; + +/** + * Decode a persisted resume cursor into the upstream `ses_…` id. Anything + * that isn't a current-version cursor with a non-empty id means "no resume" + * rather than an error. Re-adopting the session id IS the resume mechanism — + * OpenCode scopes a conversation's history by session id. + */ +function parseOpenCodeResume(raw: unknown): { readonly sessionId: string } | undefined { + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { + return undefined; + } + const record = raw as Record; + if (record.schemaVersion !== OPENCODE_RESUME_VERSION) { + return undefined; + } + if (typeof record.sessionId !== "string" || record.sessionId.trim().length === 0) { + return undefined; + } + return { sessionId: record.sessionId.trim() }; +} + +/** + * Whether an error definitively reports a missing session. Only a confirmed + * miss may silently start a fresh session; any other failure (the SDK client + * is `throwOnError: true`, so `session.get` rejects on every non-2xx) must + * propagate, or a transient blip resets a live thread to an empty one — the + * #3604 silent context loss. Decides on structured signals only, never free + * text: a numeric 404 or the exact `NotFoundError` name, found via a bounded walk + * over `cause`/`body`/`error`/`data`. An explicit non-404 status seals its + * subtree so a wrapped "NotFound" name can't reclassify a real failure. + * Exported for unit testing. + */ +export function isOpenCodeNotFound(cause: unknown): boolean { + const seen = new Set(); + const queue: Array = [cause]; + for (let steps = 0; queue.length > 0 && steps < 32; steps += 1) { + const node = queue.shift(); + if (node === null || typeof node !== "object" || seen.has(node)) { + continue; + } + seen.add(node); + const record = node as Record; + + const response = record.response; + const statuses = [ + record.status, + record.statusCode, + response !== null && typeof response === "object" + ? (response as { readonly status?: unknown }).status + : undefined, + ].filter((status): status is number => typeof status === "number"); + if (statuses.includes(404)) { + return true; + } + if (statuses.length > 0) { + continue; + } + + const name = record.name; + if (typeof name === "string" && name.toLowerCase() === "notfounderror") { + return true; + } + + for (const key of ["cause", "body", "error", "data"] as const) { + if (record[key] !== undefined) { + queue.push(record[key]); + } + } + } + return false; +} + +/** + * Whether two directory spellings name the same location. Raw string + * equality misreads a trailing slash, `.`/`..` segment, or symlinked cwd + * (macOS `/tmp` → `/private/tmp`) as a cwd change, needlessly forking the + * session on every resume. Lexically equal paths short-circuit; otherwise + * both sides go through `realPath`, each falling back to its lexical form + * on failure (deleted directory, external-server path) — so the probe can + * only widen matches, never split them. Takes the services as arguments so + * adapter methods stay service-free. Exported for unit testing. + */ +export function isSameOpenCodeDirectory( + fileSystem: FileSystem.FileSystem, + path: Path.Path, + left: string, + right: string, +): Effect.Effect { + const lexicalLeft = path.resolve(left); + const lexicalRight = path.resolve(right); + if (lexicalLeft === lexicalRight) { + return Effect.succeed(true); + } + const canonicalize = (lexical: string) => + fileSystem.realPath(lexical).pipe(Effect.orElseSucceed(() => lexical)); + return Effect.zipWith( + canonicalize(lexicalLeft), + canonicalize(lexicalRight), + (canonicalLeft, canonicalRight) => canonicalLeft === canonicalRight, + ); +} + interface OpenCodeTurnSnapshot { readonly id: TurnId; readonly items: Array; @@ -459,6 +569,10 @@ export function makeOpenCodeAdapter( const serverConfig = yield* ServerConfig; const openCodeRuntime = yield* OpenCodeRuntime; const crypto = yield* Crypto.Crypto; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const sameDirectory = (left: string, right: string) => + isSameOpenCodeDirectory(fileSystem, path, left, right); const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined @@ -1076,6 +1190,7 @@ export function makeOpenCodeAdapter( const serverUrl = openCodeSettings.serverUrl; const serverPassword = openCodeSettings.serverPassword; const directory = input.cwd ?? serverConfig.cwd; + const resumeSessionId = parseOpenCodeResume(input.resumeCursor)?.sessionId; const existing = sessions.get(input.threadId); if (existing) { yield* stopOpenCodeContext(existing); @@ -1115,22 +1230,96 @@ export function makeOpenCodeAdapter( }), ); } - const openCodeSession = yield* runOpenCodeSdk("session.create", () => - client.session.create({ - permission: buildOpenCodePermissionRules(input.runtimeMode), - }), - ); - if (!openCodeSession.data) { - return yield* new OpenCodeRuntimeError({ - operation: "session.create", - detail: "OpenCode session.create returned no session payload.", - }); - } + // Resume: re-adopt the session named by the durable cursor — + // OpenCode scopes history by session id. The probe recovers only + // a confirmed not-found (start fresh); transport/auth/server + // errors propagate instead of masking as a new empty session. + const resolved = yield* Effect.gen(function* () { + const adopted = resumeSessionId + ? yield* runOpenCodeSdk("session.get", () => + client.session.get({ sessionID: resumeSessionId }), + ).pipe( + Effect.map((response) => response.data), + Effect.catchIf( + (cause) => isOpenCodeNotFound(cause), + () => Effect.void, + ), + ) + : undefined; + + // Reuse in place only when the session still matches the + // requested cwd; on a cwd change it is forked below instead. + const reusable = + adopted && + (!adopted.directory || (yield* sameDirectory(adopted.directory, directory))) + ? adopted + : undefined; + + if (reusable) { + // Resume skips `session.create`, so re-assert the ruleset — + // a runtime-mode change would otherwise leave the session on + // its original permissions. + yield* runOpenCodeSdk("session.update", () => + client.session.update({ + sessionID: reusable.id, + permission: buildOpenCodePermissionRules(input.runtimeMode), + }), + ); + return { openCodeSession: reusable, created: false }; + } + + // The session lives under a different cwd (e.g. the thread + // moved into a git worktree). Fork it into the requested + // directory instead of minting an empty one — the fork carries + // the full history, so the follow-up keeps its context (#3604). + if (adopted) { + yield* Effect.logInfo( + `OpenCode session '${adopted.id}' was created under a different working directory; forking into '${directory}' to preserve conversation history.`, + ); + const forkedSession = yield* runOpenCodeSdk("session.fork", () => + client.session.fork({ sessionID: adopted.id, directory }), + ); + const forked = forkedSession.data; + if (!forked) { + return yield* new OpenCodeRuntimeError({ + operation: "session.fork", + detail: "OpenCode session.fork returned no session payload.", + }); + } + yield* runOpenCodeSdk("session.update", () => + client.session.update({ + sessionID: forked.id, + permission: buildOpenCodePermissionRules(input.runtimeMode), + }), + ); + return { openCodeSession: forked, created: true }; + } + + if (resumeSessionId) { + yield* Effect.logWarning( + `OpenCode session '${resumeSessionId}' no longer exists; starting a fresh session.`, + ); + } + const createdSession = yield* runOpenCodeSdk("session.create", () => + client.session.create({ + permission: buildOpenCodePermissionRules(input.runtimeMode), + }), + ); + if (!createdSession.data) { + return yield* new OpenCodeRuntimeError({ + operation: "session.create", + detail: "OpenCode session.create returned no session payload.", + }); + } + return { openCodeSession: createdSession.data, created: true }; + }); + return { sessionScope, server, client, - openCodeSession: openCodeSession.data, + openCodeSession: resolved.openCodeSession, + created: resolved.created, }; }).pipe(Effect.provideService(Scope.Scope, sessionScope)), ); @@ -1145,13 +1334,16 @@ export function makeOpenCodeAdapter( // and already inserted a session while we were awaiting async work. const raceWinner = sessions.get(input.threadId); if (raceWinner) { - // Another call won the race – clean up the session we just created - // (including the remote SDK session) and return the existing one. - yield* runOpenCodeSdk("session.abort", () => - started.client.session.abort({ - sessionID: started.openCodeSession.id, - }), - ).pipe(Effect.ignore); + // Another call won the race — clean up. Only abort the remote + // session if we created it here; a resumed one is shared upstream + // state the winner is now using. + if (started.created) { + yield* runOpenCodeSdk("session.abort", () => + started.client.session.abort({ + sessionID: started.openCodeSession.id, + }), + ).pipe(Effect.ignore); + } yield* Scope.close(started.sessionScope, Exit.void).pipe(Effect.ignore); return raceWinner.session; } @@ -1165,6 +1357,13 @@ export function makeOpenCodeAdapter( cwd: directory, ...(input.modelSelection ? { model: input.modelSelection.model } : {}), threadId: input.threadId, + // ProviderService persists this cursor and feeds it back into + // `startSession` after the in-memory session is lost (reaper / + // restart), so follow-ups continue the same conversation (#3604). + resumeCursor: { + schemaVersion: OPENCODE_RESUME_VERSION, + sessionId: started.openCodeSession.id, + }, createdAt, updatedAt: createdAt, }; @@ -1330,6 +1529,11 @@ export function makeOpenCodeAdapter( return { threadId: input.threadId, turnId, + // Re-surface the durable cursor on every turn so the persisted binding + // is refreshed alongside last-seen/runtime state (mirrors Grok/Codex). + ...(context.session.resumeCursor !== undefined + ? { resumeCursor: context.session.resumeCursor } + : {}), }; }); From 0ca3240691bf1773802b3ed70330515d68b0a6b8 Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:52:00 +0530 Subject: [PATCH 33/39] fix(server): use CLI for OpenCode health check instead of spawning server (#4153) --- .../provider/Layers/OpenCodeAdapter.test.ts | 8 + .../provider/Layers/OpenCodeProvider.test.ts | 22 +- .../src/provider/Layers/OpenCodeProvider.ts | 42 ++-- .../opencodeRuntime.cliParsers.test.ts | 229 ++++++++++++++++++ apps/server/src/provider/opencodeRuntime.ts | 176 ++++++++++++++ .../OpenCodeTextGeneration.test.ts | 8 + 6 files changed, 465 insertions(+), 20 deletions(-) create mode 100644 apps/server/src/provider/opencodeRuntime.cliParsers.test.ts diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts index 9e7211dc3e8..1385ccbaabe 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.test.ts @@ -221,6 +221,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { cause: null, }), ), + loadInventoryFromCli: () => + Effect.fail( + new OpenCodeRuntimeError({ + operation: "loadInventoryFromCli", + detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test", + cause: null, + }), + ), }; const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, { diff --git a/apps/server/src/provider/Layers/OpenCodeProvider.test.ts b/apps/server/src/provider/Layers/OpenCodeProvider.test.ts index b0e785512dc..05160517bfe 100644 --- a/apps/server/src/provider/Layers/OpenCodeProvider.test.ts +++ b/apps/server/src/provider/Layers/OpenCodeProvider.test.ts @@ -95,6 +95,13 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = { }), ) : Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory), + loadInventoryFromCli: () => + runtimeMock.state.inventoryError + ? Effect.succeed({ + providerList: { all: [], default: {}, connected: [] as string[] }, + agents: [], + } as OpenCodeInventory) + : Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory), }; beforeEach(() => { @@ -197,11 +204,22 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => { }), ); - it.effect("closes the local OpenCode server scope after provider refresh", () => + it.effect("does not spawn a local server for health check (uses CLI instead)", () => Effect.gen(function* () { yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd()); - NodeAssert.equal(runtimeMock.state.closeCalls, 1); + NodeAssert.equal(runtimeMock.state.closeCalls, 0); + }), + ); + + it.effect("degrades gracefully on CLI failure for local installs", () => + Effect.gen(function* () { + runtimeMock.state.inventoryError = new Error("opencode models failed"); + const snapshot = yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd()); + + NodeAssert.equal(snapshot.status, "warning"); + NodeAssert.equal(snapshot.installed, true); + NodeAssert.equal(snapshot.models.length, 0); }), ); }); diff --git a/apps/server/src/provider/Layers/OpenCodeProvider.ts b/apps/server/src/provider/Layers/OpenCodeProvider.ts index a8285e960fc..2c63350014d 100644 --- a/apps/server/src/provider/Layers/OpenCodeProvider.ts +++ b/apps/server/src/provider/Layers/OpenCodeProvider.ts @@ -409,26 +409,32 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu } const inventoryExit = yield* Effect.exit( - Effect.scoped( - Effect.gen(function* () { - const server = yield* openCodeRuntime.connectToOpenCodeServer({ + (isExternalServer + ? Effect.scoped( + Effect.gen(function* () { + const server = yield* openCodeRuntime.connectToOpenCodeServer({ + binaryPath: openCodeSettings.binaryPath, + serverUrl: openCodeSettings.serverUrl, + environment: resolvedEnvironment, + }); + return yield* openCodeRuntime.loadOpenCodeInventory( + openCodeRuntime.createOpenCodeSdkClient({ + baseUrl: server.url, + directory: cwd, + ...(openCodeSettings.serverPassword + ? { serverPassword: openCodeSettings.serverPassword } + : {}), + }), + ); + }), + ) + : openCodeRuntime.loadInventoryFromCli({ binaryPath: openCodeSettings.binaryPath, - serverUrl: openCodeSettings.serverUrl, environment: resolvedEnvironment, - }); - return yield* openCodeRuntime.loadOpenCodeInventory( - openCodeRuntime.createOpenCodeSdkClient({ - baseUrl: server.url, - directory: cwd, - ...(isExternalServer && openCodeSettings.serverPassword - ? { serverPassword: openCodeSettings.serverPassword } - : {}), - }), - ); - }).pipe( - Effect.mapError( - (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }), - ), + }) + ).pipe( + Effect.mapError( + (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }), ), ), ); diff --git a/apps/server/src/provider/opencodeRuntime.cliParsers.test.ts b/apps/server/src/provider/opencodeRuntime.cliParsers.test.ts new file mode 100644 index 00000000000..6208f04507e --- /dev/null +++ b/apps/server/src/provider/opencodeRuntime.cliParsers.test.ts @@ -0,0 +1,229 @@ +import * as NodeAssert from "node:assert/strict"; + +import { describe, it } from "vite-plus/test"; + +import { parseModelsCliOutput, parseAgentListCliOutput } from "./opencodeRuntime.ts"; + +describe("parseModelsCliOutput", () => { + it("parses a single model from a single provider", () => { + const stdout = [ + "anthropic/claude-sonnet-4-5", + JSON.stringify({ + id: "claude-sonnet-4-5", + providerID: "anthropic", + name: "Claude Sonnet 4.5", + capabilities: { temperature: true, reasoning: true, toolcall: true }, + cost: { input: 3, output: 15 }, + limit: { context: 200000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + release_date: "2025-01-01", + }), + ].join("\n"); + + const result = parseModelsCliOutput(stdout); + NodeAssert.equal(result.providers.size, 1); + NodeAssert.equal(result.connected.length, 1); + NodeAssert.equal(result.connected[0], "anthropic"); + + const provider = result.providers.get("anthropic")!; + NodeAssert.ok(provider); + NodeAssert.equal(provider.id, "anthropic"); + NodeAssert.equal(provider.name, "anthropic"); + NodeAssert.equal(Object.keys(provider.models).length, 1); + + const model = provider.models["claude-sonnet-4-5"]!; + NodeAssert.ok(model); + NodeAssert.equal(model.id, "claude-sonnet-4-5"); + NodeAssert.equal(model.providerID, "anthropic"); + NodeAssert.equal(model.name, "Claude Sonnet 4.5"); + }); + + it("parses multiple models from multiple providers", () => { + const stdout = [ + "anthropic/claude-sonnet-4-5", + JSON.stringify({ id: "claude-sonnet-4-5", providerID: "anthropic", name: "Sonnet 4.5" }), + "anthropic/claude-haiku-4-5", + JSON.stringify({ id: "claude-haiku-4-5", providerID: "anthropic", name: "Haiku 4.5" }), + "openai/gpt-4o", + JSON.stringify({ id: "gpt-4o", providerID: "openai", name: "GPT-4o" }), + ].join("\n"); + + const result = parseModelsCliOutput(stdout); + NodeAssert.equal(result.providers.size, 2); + NodeAssert.equal(result.connected.length, 2); + NodeAssert.equal([...result.connected].sort().join(","), "anthropic,openai"); + NodeAssert.equal(Object.keys(result.providers.get("anthropic")!.models).length, 2); + NodeAssert.equal(Object.keys(result.providers.get("openai")!.models).length, 1); + }); + + it("handles empty input", () => { + const result = parseModelsCliOutput(""); + NodeAssert.equal(result.providers.size, 0); + NodeAssert.equal(result.connected.length, 0); + }); + + it("skips unparseable JSON blocks", () => { + const stdout = [ + "anthropic/claude-sonnet-4-5", + "this is not valid json {{{", + "anthropic/claude-haiku-4-5", + JSON.stringify({ id: "claude-haiku-4-5", providerID: "anthropic", name: "Haiku 4.5" }), + ].join("\n"); + + const result = parseModelsCliOutput(stdout); + NodeAssert.equal(result.providers.size, 1); + const provider = result.providers.get("anthropic")!; + NodeAssert.equal(Object.keys(provider.models).length, 1); + NodeAssert.ok(provider.models["claude-haiku-4-5"]); + }); + + it("handles Windows-style CRLF line endings", () => { + const stdout = + "anthropic/claude-sonnet-4-5\r\n" + + JSON.stringify({ id: "claude-sonnet-4-5", providerID: "anthropic", name: "Sonnet" }) + + "\r\n"; + + const result = parseModelsCliOutput(stdout); + NodeAssert.equal(result.providers.size, 1); + NodeAssert.ok(result.providers.get("anthropic")!.models["claude-sonnet-4-5"]); + }); + + it("handles model JSON with variants and nested fields", () => { + const stdout = [ + "opencode/gpt-5.4", + JSON.stringify({ + id: "gpt-5.4", + providerID: "opencode", + name: "GPT-5.4", + family: "gpt", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 200000, input: 160000, output: 32000 }, + status: "active", + options: {}, + headers: {}, + release_date: "2025-01-01", + variants: { none: {}, low: {}, medium: {}, high: {} }, + }), + ].join("\n"); + + const result = parseModelsCliOutput(stdout); + const model = result.providers.get("opencode")!.models["gpt-5.4"]!; + NodeAssert.ok(model); + NodeAssert.ok(model.capabilities); + NodeAssert.equal(model.capabilities!.reasoning, true); + NodeAssert.ok(model.variants); + NodeAssert.equal(model.variants!["medium"] !== undefined, true); + }); +}); + +describe("parseAgentListCliOutput", () => { + it("parses a single agent", () => { + const stdout = [ + "build (primary)", + " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]), + ].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result.length, 1); + NodeAssert.equal(result[0]!.name, "build"); + NodeAssert.equal(result[0]!.mode, "primary"); + NodeAssert.equal(result[0]!.permission.length, 1); + }); + + it("parses multiple agents", () => { + const stdout = [ + "build (primary)", + " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]), + "explore (subagent)", + " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]), + "plan (primary)", + " " + JSON.stringify([{ permission: "edit", action: "ask", pattern: "*.md" }]), + ].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result.length, 3); + NodeAssert.equal(result[0]!.name, "build"); + NodeAssert.equal(result[0]!.mode, "primary"); + NodeAssert.equal(result[1]!.name, "explore"); + NodeAssert.equal(result[1]!.mode, "subagent"); + NodeAssert.equal(result[2]!.name, "plan"); + NodeAssert.equal(result[2]!.mode, "primary"); + }); + + it("handles empty input", () => { + const result = parseAgentListCliOutput(""); + NodeAssert.equal(result.length, 0); + }); + + it("skips agents with unparseable permission JSON", () => { + const stdout = [ + "build (primary)", + " not valid json {", + "explore (subagent)", + " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]), + ].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result.length, 1); + NodeAssert.equal(result[0]!.name, "explore"); + }); + + it("handles real-world permission blocks with nested paths", () => { + const permissions = [ + { permission: "*", action: "allow", pattern: "*" }, + { + permission: "external_directory", + pattern: "C:\\Users\\test\\.local\\*", + action: "allow", + }, + { permission: "read", pattern: "*.env", action: "ask" }, + ]; + const stdout = ["build (primary)", " " + JSON.stringify(permissions)].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result.length, 1); + NodeAssert.equal(result[0]!.permission.length, 3); + NodeAssert.equal(result[0]!.permission[0]!.action, "allow"); + NodeAssert.equal(result[0]!.permission[2]!.action, "ask"); + }); + + it("handles agent names with spaces", () => { + const stdout = [ + "code reviewer (subagent)", + " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]), + "my custom agent (primary)", + " " + JSON.stringify([{ permission: "edit", action: "ask", pattern: "*.ts" }]), + ].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result.length, 2); + NodeAssert.equal(result[0]!.name, "code reviewer"); + NodeAssert.equal(result[0]!.mode, "subagent"); + NodeAssert.equal(result[1]!.name, "my custom agent"); + NodeAssert.equal(result[1]!.mode, "primary"); + }); + + it("marks known hidden agents", () => { + const stdout = [ + "compaction (primary)", + " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]), + "build (primary)", + " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]), + ].join("\n"); + + const result = parseAgentListCliOutput(stdout); + NodeAssert.equal(result[0]!.hidden, true); + NodeAssert.equal(result[1]!.hidden, false); + }); +}); diff --git a/apps/server/src/provider/opencodeRuntime.ts b/apps/server/src/provider/opencodeRuntime.ts index 46d0b3019bd..b853662b037 100644 --- a/apps/server/src/provider/opencodeRuntime.ts +++ b/apps/server/src/provider/opencodeRuntime.ts @@ -5,6 +5,7 @@ import { createOpencodeClient, type Agent, type FilePartInput, + type Model, type OpencodeClient, type PermissionRuleset, type ProviderListResponse, @@ -147,6 +148,10 @@ export interface OpenCodeRuntimeShape { readonly loadOpenCodeInventory: ( client: OpencodeClient, ) => Effect.Effect; + readonly loadInventoryFromCli: (input: { + readonly binaryPath: string; + readonly environment?: NodeJS.ProcessEnv; + }) => Effect.Effect; } function parseServerUrlFromOutput(output: string): string | null { @@ -160,6 +165,113 @@ function parseServerUrlFromOutput(output: string): string | null { return null; } +const SLUG_LINE_RE = /^(\S+\/\S+)\s*$/; +const AGENT_HEADER_RE = /^(.+)\s+\((\S+)\)\s*$/; + +// Agents that are always hidden in OpenCode but the CLI "agent list" command +// does not expose the hidden flag. Keep in sync with OpenCode agent +// definitions (in the OpenCode repo: packages/opencode/src/agent/agent.ts). +const KNOWN_HIDDEN_AGENTS = new Set(["compaction", "summary", "title"]); + +/** @internal */ +export function parseModelsCliOutput(stdout: string): { + readonly providers: ReadonlyMap< + string, + { readonly id: string; readonly name: string; readonly models: { [key: string]: Model } } + >; + readonly connected: ReadonlyArray; +} { + const providers = new Map< + string, + { id: string; name: string; models: { [key: string]: Model } } + >(); + const lines = stdout.split("\n"); + let currentSlug: string | null = null; + const jsonLines: Array = []; + + const flushModel = () => { + if (currentSlug !== null && jsonLines.length > 0) { + const jsonStr = jsonLines.join("\n").trim(); + if (jsonStr.length > 0) { + try { + const model = JSON.parse(jsonStr) as Model; + const separator = currentSlug.indexOf("/"); + if (separator > 0) { + const providerID = currentSlug.slice(0, separator); + const modelID = currentSlug.slice(separator + 1); + let provider = providers.get(providerID); + if (!provider) { + provider = { id: providerID, name: providerID, models: {} }; + providers.set(providerID, provider); + } + provider.models[modelID] = model; + } + } catch { + // Skip unparseable model JSON + } + } + } + currentSlug = null; + jsonLines.length = 0; + }; + + for (const line of lines) { + const slugMatch = SLUG_LINE_RE.exec(line); + if (slugMatch) { + flushModel(); + currentSlug = slugMatch[1]!; + } else if (currentSlug !== null) { + jsonLines.push(line); + } + } + flushModel(); + + return { providers, connected: [...providers.keys()] }; +} + +/** @internal */ +export function parseAgentListCliOutput(stdout: string): ReadonlyArray { + const agents: Array = []; + const lines = stdout.split("\n"); + let currentHeader: { name: string; mode: string } | null = null; + const blockLines: Array = []; + + const flushAgent = () => { + if (currentHeader !== null) { + const jsonStr = blockLines.join("\n").trim(); + if (jsonStr.length > 0) { + try { + const permission = JSON.parse(jsonStr); + agents.push({ + name: currentHeader.name, + mode: currentHeader.mode as Agent["mode"], + hidden: KNOWN_HIDDEN_AGENTS.has(currentHeader.name), + permission, + options: {}, + }); + } catch { + // Skip unparseable agent + } + } + } + currentHeader = null; + blockLines.length = 0; + }; + + for (const line of lines) { + const match = AGENT_HEADER_RE.exec(line); + if (match) { + flushAgent(); + currentHeader = { name: match[1]!, mode: match[2]! }; + } else if (currentHeader !== null) { + blockLines.push(line); + } + } + flushAgent(); + + return agents; +} + export function parseOpenCodeModelSlug( slug: string | null | undefined, ): ParsedOpenCodeModelSlug | null { @@ -542,12 +654,76 @@ const makeOpenCodeRuntime = Effect.gen(function* () { Effect.map(([providerList, agents]) => ({ providerList, agents })), ); + const loadInventoryFromCli: OpenCodeRuntimeShape["loadInventoryFromCli"] = (input) => + Effect.gen(function* () { + const env = input.environment !== undefined ? { environment: input.environment } : ({} as {}); + + const runModelsCli = () => + runOpenCodeCommand({ + binaryPath: input.binaryPath, + args: ["models", "--verbose"], + ...env, + }).pipe(Effect.exit); + const runAgentsCli = () => + runOpenCodeCommand({ binaryPath: input.binaryPath, args: ["agent", "list"], ...env }).pipe( + Effect.exit, + ); + + // First attempt — run both in parallel + let [modelsResult, agentsResult] = yield* Effect.all([runModelsCli(), runAgentsCli()], { + concurrency: "unbounded", + }); + + // Retry once after 1s on transient failures (e.g. SQLite "database is locked") + const needsModelsRetry = modelsResult._tag === "Failure" || modelsResult.value.code !== 0; + const needsAgentsRetry = agentsResult._tag === "Failure" || agentsResult.value.code !== 0; + if (needsModelsRetry || needsAgentsRetry) { + yield* Effect.sleep("1 second"); + const [m2, a2] = yield* Effect.all( + [ + needsModelsRetry ? runModelsCli() : Effect.succeed(modelsResult), + needsAgentsRetry ? runAgentsCli() : Effect.succeed(agentsResult), + ], + { concurrency: "unbounded" }, + ); + modelsResult = m2; + agentsResult = a2; + } + + // Degrade gracefully on failure — return empty inventory (warning status, not error) + let connected: string[] = []; + let allProviders: ProviderListResponse["all"] = []; + if (modelsResult._tag === "Success" && modelsResult.value.code === 0) { + const parsed = parseModelsCliOutput(modelsResult.value.stdout); + connected = [...parsed.connected]; + allProviders = [...parsed.providers.values()].map((p) => ({ + id: p.id, + name: p.name, + source: "config" as const, + env: [], + options: {}, + models: p.models, + })); + } + + let agents: ReadonlyArray = []; + if (agentsResult._tag === "Success" && agentsResult.value.code === 0) { + agents = parseAgentListCliOutput(agentsResult.value.stdout); + } + + return { + providerList: { all: allProviders, default: {}, connected }, + agents, + }; + }); + return { startOpenCodeServerProcess, connectToOpenCodeServer, runOpenCodeCommand, createOpenCodeSdkClient, loadOpenCodeInventory, + loadInventoryFromCli, } satisfies OpenCodeRuntimeShape; }); diff --git a/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts b/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts index 558a8663b64..1fcf9bc4c73 100644 --- a/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts +++ b/apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts @@ -107,6 +107,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = { cause: null, }), ), + loadInventoryFromCli: () => + Effect.fail( + new OpenCodeRuntime.OpenCodeRuntimeError({ + operation: "loadInventoryFromCli", + detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test", + cause: null, + }), + ), }; const DEFAULT_TEST_MODEL_SELECTION = { From 946b867666be94482e9e41015a304f55fe1e4754 Mon Sep 17 00:00:00 2001 From: xxashxx-svg Date: Mon, 20 Jul 2026 16:46:47 +0530 Subject: [PATCH 34/39] fix(web): scope timeline minimap hover target to the side gutter (#3869) Co-authored-by: Claude Fable 5 --- .../components/chat/MessagesTimeline.logic.ts | 39 +++++++++++ .../components/chat/MessagesTimeline.test.tsx | 24 +++++++ .../src/components/chat/MessagesTimeline.tsx | 65 ++++++++++++++----- 3 files changed, 110 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index c6e277cce08..3227bac2413 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -64,6 +64,45 @@ export function resolveTimelineMinimapHasPersistentGutter(viewportWidth: number) return sideGutter >= TIMELINE_MINIMAP_PERSISTENT_GUTTER; } +export const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12; +export const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40; +export const TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH = "22rem"; + +/** + * The minimap overlays the viewport's left edge while the content column is + * centered, so the side gutter between them shrinks under browser zoom or a + * narrow pane. A fixed-width hover strip would then sit on top of the message + * text and swallow its pointer events. Cap the strip's width so it never + * extends past the gutter into the content column; 0 disables the strip. + */ +export function resolveTimelineMinimapHitStripWidth(viewportWidth: number): number { + if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) { + return 0; + } + + const contentWidth = Math.min(viewportWidth, TIMELINE_CONTENT_MAX_WIDTH); + const sideGutter = Math.max(0, (viewportWidth - contentWidth) / 2); + return Math.max( + 0, + Math.min( + TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH, + Math.floor(sideGutter) - TIMELINE_MINIMAP_HIT_STRIP_LEFT, + ), + ); +} + +/** + * Once the preview is open, keep the full preview and the space leading to it + * interactive. The collapsed strip remains gutter-capped so it cannot block + * selecting message text. + */ +export function resolveTimelineMinimapInteractiveWidth( + collapsedWidth: number, + expanded: boolean, +): number | string { + return expanded ? TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH : collapsedWidth; +} + function computeElapsedMs(startIso: string, endIso: string): number | null { const start = Date.parse(startIso); const end = Date.parse(endIso); diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 0957e025311..a58724f3308 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -224,7 +224,9 @@ describe("MessagesTimeline", () => { resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapHeightStyle, + resolveTimelineMinimapHitStripWidth, resolveTimelineMinimapIndexFromPointer, + resolveTimelineMinimapInteractiveWidth, resolveTimelineMinimapTopPercent, } = await import("./MessagesTimeline.logic"); @@ -254,6 +256,28 @@ describe("MessagesTimeline", () => { expect(resolveTimelineMinimapHasPersistentGutter(832)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(863)).toBe(false); expect(resolveTimelineMinimapHasPersistentGutter(864)).toBe(true); + + // No usable gutter (zoomed in / narrow pane): the strip must go inert + // instead of overlaying the centered content column. + expect(resolveTimelineMinimapHitStripWidth(768)).toBe(0); + expect(resolveTimelineMinimapHitStripWidth(792)).toBe(0); + // Partial gutter: strip shrinks to what fits between the viewport edge + // and the content column. + expect(resolveTimelineMinimapHitStripWidth(820)).toBe(14); + // Full gutter: unchanged 40px-wide strip. + expect(resolveTimelineMinimapHitStripWidth(872)).toBe(40); + expect(resolveTimelineMinimapHitStripWidth(1400)).toBe(40); + expect(resolveTimelineMinimapHitStripWidth(0)).toBe(0); + expect(resolveTimelineMinimapHitStripWidth(Number.NaN)).toBe(0); + + // The collapsed target stays narrow, but an open preview keeps its full + // 20rem width plus the 2rem offset from the minimap rail interactive. + expect(resolveTimelineMinimapInteractiveWidth(0, false)).toBe(0); + expect(resolveTimelineMinimapInteractiveWidth(14, false)).toBe(14); + expect(resolveTimelineMinimapInteractiveWidth(40, false)).toBe(40); + expect(resolveTimelineMinimapInteractiveWidth(0, true)).toBe("22rem"); + expect(resolveTimelineMinimapInteractiveWidth(14, true)).toBe("22rem"); + expect(resolveTimelineMinimapInteractiveWidth(40, true)).toBe("22rem"); }); it("anchors a sent attachment message using its measured height", async () => { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 46d6a4050b8..73fc86312e5 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -73,7 +73,9 @@ import { resolveTimelineIsAtEnd, resolveTimelineMinimapHasPersistentGutter, resolveTimelineMinimapHeightStyle, + resolveTimelineMinimapHitStripWidth, resolveTimelineMinimapIndexFromPointer, + resolveTimelineMinimapInteractiveWidth, resolveTimelineMinimapTopPercent, type StableMessagesTimelineRowsState, type MessagesTimelineRow, @@ -324,6 +326,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ null, ); const [minimapHasPersistentGutter, setMinimapHasPersistentGutter] = useState(false); + const [minimapHitStripWidth, setMinimapHitStripWidth] = useState(0); const handleAnchorReady = useCallback( (info: { anchorIndex: number | undefined }) => { if (anchorMessageId !== null && info.anchorIndex !== undefined) { @@ -395,6 +398,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ setMinimapHasPersistentGutter((current) => current === nextHasPersistentGutter ? current : nextHasPersistentGutter, ); + setMinimapHitStripWidth(resolveTimelineMinimapHitStripWidth(viewportWidth)); }; const frame = requestAnimationFrame(measure); @@ -511,6 +515,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ items={minimapItems} bottomInset={contentInsetEndAdjustment} hasPersistentGutter={minimapHasPersistentGutter} + hitStripWidth={minimapHitStripWidth} stripMap={minimapStripMap} onSelect={(item) => { onManualNavigation(); @@ -605,15 +610,21 @@ function resolveTimelineRowHeight(state: TimelinePositionState, rowIndex: number return typeof height === "number" && Number.isFinite(height) ? height : null; } +function timelineMinimapEventTargetsPreview(target: EventTarget): boolean { + return target instanceof Element && target.closest("[data-minimap-preview]") !== null; +} + function TimelineMinimap({ bottomInset, hasPersistentGutter, + hitStripWidth, items, stripMap, onSelect, }: { bottomInset: number; hasPersistentGutter: boolean; + hitStripWidth: number; items: ReadonlyArray; stripMap: Map; onSelect: (item: TimelineMinimapItem) => void; @@ -676,7 +687,7 @@ function TimelineMinimap({ return ( ); }); From c710167bde19e665c8517b606718bfbd406f2dd1 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 20 Jul 2026 07:30:16 -0400 Subject: [PATCH 36/39] fix(web): paint text selection over composer chips (#4139) Signed-off-by: Yordis Prieto --- .../src/components/ComposerPromptEditor.tsx | 84 ++++++++++++++++++- apps/web/src/index.css | 12 +++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 585d46150a8..169126788ae 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -28,7 +28,10 @@ import { KEY_ENTER_COMMAND, KEY_TAB_COMMAND, COMMAND_PRIORITY_HIGH, + COMMAND_PRIORITY_LOW, KEY_BACKSPACE_COMMAND, + BLUR_COMMAND, + FOCUS_COMMAND, $getRoot, HISTORY_MERGE_TAG, DecoratorNode, @@ -185,7 +188,7 @@ class ComposerMentionNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "inline-flex align-middle leading-none"; + dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; return dom; } @@ -323,7 +326,7 @@ class ComposerSkillNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "inline-flex align-middle leading-none"; + dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; return dom; } @@ -394,7 +397,7 @@ class ComposerTerminalContextNode extends DecoratorNode { override createDOM(): HTMLElement { const dom = document.createElement("span"); - dom.className = "inline-flex align-middle leading-none"; + dom.className = "composer-inline-chip relative inline-flex align-middle leading-none"; return dom; } @@ -1167,6 +1170,80 @@ function ComposerInlineTokenBackspacePlugin() { return null; } +/** + * Chips render as non-editable decorators, so the browser never paints the + * native text selection over them; without help, a selection spanning chips + * is only visible in the slivers between them. Mirror the selection onto the + * chips with a data attribute the stylesheet turns into a highlight overlay. + */ +function ComposerChipSelectionPlugin() { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + let selectedKeys = new Set(); + // Lexical keeps the range selection on blur without emitting an update, + // so focus is tracked separately; while blurred the native highlight is + // gone and the mirrored one has to go with it. + let hasFocus = editor.getRootElement() === document.activeElement; + + const applyKeys = (nextKeys: Set) => { + for (const key of selectedKeys) { + if (!nextKeys.has(key)) { + editor.getElementByKey(key)?.removeAttribute("data-composer-chip-selected"); + } + } + for (const key of nextKeys) { + editor.getElementByKey(key)?.setAttribute("data-composer-chip-selected", "true"); + } + selectedKeys = nextKeys; + }; + + const readSelectedKeys = () => { + const nextKeys = new Set(); + editor.getEditorState().read(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection) && !selection.isCollapsed()) { + for (const node of selection.getNodes()) { + if (node instanceof DecoratorNode) { + nextKeys.add(node.getKey()); + } + } + } + }); + return nextKeys; + }; + + const unregisterUpdate = editor.registerUpdateListener(() => { + applyKeys(hasFocus ? readSelectedKeys() : new Set()); + }); + const unregisterFocus = editor.registerCommand( + FOCUS_COMMAND, + () => { + hasFocus = true; + applyKeys(readSelectedKeys()); + return false; + }, + COMMAND_PRIORITY_LOW, + ); + const unregisterBlur = editor.registerCommand( + BLUR_COMMAND, + () => { + hasFocus = false; + applyKeys(new Set()); + return false; + }, + COMMAND_PRIORITY_LOW, + ); + return () => { + unregisterUpdate(); + unregisterFocus(); + unregisterBlur(); + }; + }, [editor]); + + return null; +} + function ComposerInlineTokenPastePlugin() { const [editor] = useLexicalComposerContext(); @@ -1701,6 +1778,7 @@ function ComposerPromptEditorInner({ +
diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 77a5f027c13..41abcbd3b17 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1067,3 +1067,15 @@ label:has(> select#reasoning-effort) select { .dark .model-picker-list::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.15); } + +/* Composer chips are non-editable decorators, so the browser skips them when + painting text selection; this overlay stands in for the native highlight. */ +.composer-inline-chip[data-composer-chip-selected]::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 6px; + background-color: Highlight; + opacity: 0.3; + pointer-events: none; +} From fa69f05b69d05003a6994194c12edd1d8aee080c Mon Sep 17 00:00:00 2001 From: Maxwell Young Date: Mon, 20 Jul 2026 23:32:30 +1200 Subject: [PATCH 37/39] [codex] preserve custom model slugs (#4168) --- .../src/provider/Layers/ClaudeProvider.ts | 5 --- .../src/provider/Layers/CursorProvider.ts | 5 +-- .../src/provider/Layers/GrokProvider.ts | 9 +---- .../src/provider/Layers/OpenCodeProvider.ts | 25 ++------------ .../src/provider/providerSnapshot.test.ts | 22 +++++++++++-- apps/server/src/provider/providerSnapshot.ts | 5 ++- .../settings/ProviderModelsSection.tsx | 4 +-- apps/web/src/modelSelection.test.ts | 33 +++++++++++++++++++ apps/web/src/modelSelection.ts | 10 +++--- packages/shared/src/model.test.ts | 13 +++++++- packages/shared/src/model.ts | 15 ++++++--- 11 files changed, 88 insertions(+), 58 deletions(-) diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index 94b24405eee..ff0d1992454 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -2,7 +2,6 @@ import { type ClaudeSettings, type ModelCapabilities, type ModelSelection, - ProviderDriverKind, type ServerProviderModel, type ServerProviderSlashCommand, } from "@t3tools/contracts"; @@ -44,7 +43,6 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabili optionDescriptors: [], }); -const PROVIDER = ProviderDriverKind.make("claudeAgent"); const CLAUDE_PRESENTATION = { displayName: "Claude", showInteractionModeToggle: true, @@ -691,7 +689,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( const checkedAt = DateTime.formatIso(yield* DateTime.now); const allModels = providerModelsFromSettings( BUILT_IN_MODELS, - PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); @@ -782,7 +779,6 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( const models = providerModelsFromSettings( getBuiltInClaudeModelsForVersion(parsedVersion), - PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); @@ -851,7 +847,6 @@ export const makePendingClaudeProvider = ( const checkedAt = yield* nowIso; const models = providerModelsFromSettings( BUILT_IN_MODELS, - PROVIDER, claudeSettings.customModels, DEFAULT_CLAUDE_MODEL_CAPABILITIES, ); diff --git a/apps/server/src/provider/Layers/CursorProvider.ts b/apps/server/src/provider/Layers/CursorProvider.ts index 063f2c1bf2b..fee4306c4c5 100644 --- a/apps/server/src/provider/Layers/CursorProvider.ts +++ b/apps/server/src/provider/Layers/CursorProvider.ts @@ -8,7 +8,6 @@ import type { ServerProviderModel, ServerProviderState, } from "@t3tools/contracts"; -import { ProviderDriverKind } from "@t3tools/contracts"; import type * as EffectAcpSchema from "effect-acp/schema"; import { causeErrorTag } from "@t3tools/shared/observability"; import * as Crypto from "effect/Crypto"; @@ -51,7 +50,6 @@ import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts" const decodeCursorListAvailableModelsResponse = Schema.decodeUnknownEffect( CursorListAvailableModelsResponse, ); -const PROVIDER = ProviderDriverKind.make("cursor"); const CURSOR_PRESENTATION = { displayName: "Cursor", badgeLabel: "Early Access", @@ -576,7 +574,7 @@ export const discoverCursorModelsViaAcp = ( export function getCursorFallbackModels( cursorSettings: Pick, ): ReadonlyArray { - return providerModelsFromSettings([], PROVIDER, cursorSettings.customModels, EMPTY_CAPABILITIES); + return providerModelsFromSettings([], cursorSettings.customModels, EMPTY_CAPABILITIES); } /** Timeout for `agent about` — it's slower than a simple `--version` probe. */ @@ -638,7 +636,6 @@ export function buildCursorProviderSnapshot(input: { checkedAt: input.checkedAt, models: providerModelsFromSettings( input.discoveredModels ?? [], - PROVIDER, input.cursorSettings.customModels, EMPTY_CAPABILITIES, ), diff --git a/apps/server/src/provider/Layers/GrokProvider.ts b/apps/server/src/provider/Layers/GrokProvider.ts index 33f61ad97f6..934eecdb5ae 100644 --- a/apps/server/src/provider/Layers/GrokProvider.ts +++ b/apps/server/src/provider/Layers/GrokProvider.ts @@ -1,7 +1,6 @@ import { type GrokSettings, type ModelCapabilities, - ProviderDriverKind, type ServerProvider, type ServerProviderModel, } from "@t3tools/contracts"; @@ -38,7 +37,6 @@ const GROK_PRESENTATION = { showInteractionModeToggle: false, requiresNewThreadForModelChange: true, } as const; -const PROVIDER = ProviderDriverKind.make("grok"); const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [], }); @@ -98,12 +96,7 @@ function grokModelsFromSettings( customModels: ReadonlyArray | undefined, builtInModels: ReadonlyArray = GROK_BUILT_IN_MODELS, ): ReadonlyArray { - return providerModelsFromSettings( - builtInModels, - PROVIDER, - customModels ?? [], - EMPTY_CAPABILITIES, - ); + return providerModelsFromSettings(builtInModels, customModels ?? [], EMPTY_CAPABILITIES); } function buildGrokDiscoveredModelsFromSessionModelState( diff --git a/apps/server/src/provider/Layers/OpenCodeProvider.ts b/apps/server/src/provider/Layers/OpenCodeProvider.ts index 2c63350014d..21014e33f08 100644 --- a/apps/server/src/provider/Layers/OpenCodeProvider.ts +++ b/apps/server/src/provider/Layers/OpenCodeProvider.ts @@ -1,5 +1,4 @@ import { - ProviderDriverKind, type ModelCapabilities, type OpenCodeSettings, type ServerProviderModel, @@ -25,7 +24,6 @@ import { } from "../opencodeRuntime.ts"; import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2"; -const PROVIDER = ProviderDriverKind.make("opencode"); const OPENCODE_PRESENTATION = { displayName: "OpenCode", showInteractionModeToggle: false, @@ -259,7 +257,6 @@ export const makePendingOpenCodeProvider = ( const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso); const models = providerModelsFromSettings( [], - PROVIDER, openCodeSettings.customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES, ); @@ -319,12 +316,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu presentation: OPENCODE_PRESENTATION, enabled: openCodeSettings.enabled, checkedAt, - models: providerModelsFromSettings( - [], - PROVIDER, - customModels, - DEFAULT_OPENCODE_MODEL_CAPABILITIES, - ), + models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), probe: { installed: failure.installed, version, @@ -340,12 +332,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu presentation: OPENCODE_PRESENTATION, enabled: false, checkedAt, - models: providerModelsFromSettings( - [], - PROVIDER, - customModels, - DEFAULT_OPENCODE_MODEL_CAPABILITIES, - ), + models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), probe: { installed: false, version: null, @@ -391,12 +378,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu presentation: OPENCODE_PRESENTATION, enabled: openCodeSettings.enabled, checkedAt, - models: providerModelsFromSettings( - [], - PROVIDER, - customModels, - DEFAULT_OPENCODE_MODEL_CAPABILITIES, - ), + models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES), probe: { installed: true, version, @@ -444,7 +426,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu const models = providerModelsFromSettings( flattenOpenCodeModels(inventoryExit.value), - PROVIDER, customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES, ); diff --git a/apps/server/src/provider/providerSnapshot.test.ts b/apps/server/src/provider/providerSnapshot.test.ts index abe138fdfb9..01157278066 100644 --- a/apps/server/src/provider/providerSnapshot.test.ts +++ b/apps/server/src/provider/providerSnapshot.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts"; +import type { ModelCapabilities } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as Effect from "effect/Effect"; @@ -38,7 +38,6 @@ describe("providerModelsFromSettings", () => { it("applies the provided capabilities to custom models", () => { const models = providerModelsFromSettings( [], - ProviderDriverKind.make("opencode"), ["openai/gpt-5"], OPENCODE_CUSTOM_MODEL_CAPABILITIES, ); @@ -52,6 +51,25 @@ describe("providerModelsFromSettings", () => { }, ]); }); + + it("preserves a custom slug that collides with a provider alias", () => { + const capabilities = createModelCapabilities({ optionDescriptors: [] }); + const models = providerModelsFromSettings( + [ + { + slug: "claude-opus-4-8", + name: "Claude Opus 4.8", + isCustom: false, + capabilities, + }, + ], + [" opus "], + capabilities, + ); + + expect(models.map((model) => model.slug)).toEqual(["claude-opus-4-8", "opus"]); + expect(models[1]?.isCustom).toBe(true); + }); }); describe("ProviderCommandNotFoundError", () => { diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index dfe31ffdc44..e741a7a2c1d 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -13,7 +13,7 @@ import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { normalizeModelSlug } from "@t3tools/shared/model"; +import { normalizeCustomModelSlug } from "@t3tools/shared/model"; import { isWindowsCommandNotFound } from "../processRunner.ts"; import { createProviderVersionAdvisory } from "./providerMaintenance.ts"; import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; @@ -140,7 +140,6 @@ export function parseGenericCliVersion(output: string): string | null { export function providerModelsFromSettings( builtInModels: ReadonlyArray, - provider: ProviderDriverKind, customModels: ReadonlyArray, customModelCapabilities: ModelCapabilities, ): ReadonlyArray { @@ -149,7 +148,7 @@ export function providerModelsFromSettings( const customEntries: ServerProviderModel[] = []; for (const candidate of customModels) { - const normalized = normalizeModelSlug(candidate, provider); + const normalized = normalizeCustomModelSlug(candidate); if (!normalized || seen.has(normalized)) { continue; } diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index f1100ab93d2..27d13997552 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -16,7 +16,7 @@ import { type ProviderInstanceId, type ServerProviderModel, } from "@t3tools/contracts"; -import { normalizeModelSlug } from "@t3tools/shared/model"; +import { normalizeCustomModelSlug } from "@t3tools/shared/model"; import { cn } from "../../lib/utils"; import { sortModelsForProviderInstance } from "../../modelOrdering"; @@ -111,7 +111,7 @@ export function ProviderModelsSection({ }, [favoriteModelSet, modelOrder, models]); const handleAdd = () => { - const normalized = driverKind ? normalizeModelSlug(input, driverKind) : input.trim() || null; + const normalized = normalizeCustomModelSlug(input); if (!normalized) { setError("Enter a model slug."); return; diff --git a/apps/web/src/modelSelection.test.ts b/apps/web/src/modelSelection.test.ts index 3d973ccca74..e8fc8a244d0 100644 --- a/apps/web/src/modelSelection.test.ts +++ b/apps/web/src/modelSelection.test.ts @@ -101,6 +101,39 @@ describe("instance-scoped model selection", () => { ).toBe("openai/gpt-5.5"); }); + it("preserves a custom slug that collides with a provider alias", () => { + const providers = [ + provider({ + provider: ProviderDriverKind.make("claudeAgent"), + instanceId: "claude_openrouter", + models: ["claude-opus-4-8"], + }), + ]; + const settings: UnifiedSettings = { + ...settingsWithProviderInstances(), + providerInstances: { + ...settingsWithProviderInstances().providerInstances, + [ProviderInstanceId.make("claude_openrouter")]: { + driver: ProviderDriverKind.make("claudeAgent"), + config: { customModels: ["opus"] }, + }, + }, + }; + const openrouter = deriveProviderInstanceEntries(providers)[0]!; + + expect( + getAppModelOptionsForInstance(settings, openrouter).map((option) => option.slug), + ).toEqual(["claude-opus-4-8", "opus"]); + expect( + resolveAppModelSelectionForInstance( + ProviderInstanceId.make("claude_openrouter"), + settings, + providers, + "opus", + ), + ).toBe("opus"); + }); + it("includes Grok custom models from the selected provider instance", () => { const providers = [provider({ provider: ProviderDriverKind.make("grok"), instanceId: "grok" })]; const settings: UnifiedSettings = { diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 7ede9665ac9..f1d527880ed 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -9,7 +9,7 @@ import { } from "@t3tools/contracts"; import { createModelSelection, - normalizeModelSlug, + normalizeCustomModelSlug, resolveSelectableModel, } from "@t3tools/shared/model"; import { getComposerProviderState } from "./components/chat/composerProviderState"; @@ -117,13 +117,12 @@ function applyInstanceModelPreferences( export function normalizeCustomModelSlugs( models: Iterable, builtInModelSlugs: ReadonlySet, - provider: ProviderDriverKind = ProviderDriverKind.make("codex"), ): string[] { const normalizedModels: string[] = []; const seen = new Set(); for (const candidate of models) { - const normalized = normalizeModelSlug(candidate, provider); + const normalized = normalizeCustomModelSlug(candidate); if ( !normalized || normalized.length > MAX_CUSTOM_MODEL_LENGTH || @@ -163,7 +162,7 @@ export function getAppModelOptions( // see the user's authored custom models. const defaultInstanceId = defaultInstanceIdForDriver(provider); const customModels = readInstanceCustomModels(settings, defaultInstanceId, provider); - for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, provider)) { + for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) { if (seen.has(slug)) { continue; } @@ -206,8 +205,7 @@ export function getAppModelOptionsForInstance( ); const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind); - const normalizer = entry.driverKind; - for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs, normalizer)) { + for (const slug of normalizeCustomModelSlugs(customModels, builtInModelSlugs)) { if (seen.has(slug)) { continue; } diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts index ee91e0b53f7..b055c255bd7 100644 --- a/packages/shared/src/model.test.ts +++ b/packages/shared/src/model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; -import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts"; +import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts"; import { buildProviderOptionSelectionsFromDescriptors, @@ -10,6 +10,8 @@ import { getProviderOptionDescriptors, getProviderOptionBooleanSelectionValue, getProviderOptionStringSelectionValue, + normalizeCustomModelSlug, + normalizeModelSlug, } from "./model.ts"; const codexCaps: ModelCapabilities = createModelCapabilities({ @@ -144,3 +146,12 @@ describe("descriptor helpers", () => { expect(getModelSelectionBooleanOptionValue(selection, "fastMode")).toBe(true); }); }); + +describe("model slug normalization", () => { + it("preserves exact custom slugs instead of expanding provider aliases", () => { + const claude = ProviderDriverKind.make("claudeAgent"); + + expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-4-8"); + expect(normalizeCustomModelSlug(" opus ")).toBe("opus"); + }); +}); diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts index 9fdbd6c0447..bdc0c0cc8ef 100644 --- a/packages/shared/src/model.ts +++ b/packages/shared/src/model.ts @@ -236,11 +236,7 @@ export function normalizeModelSlug( model: string | null | undefined, provider: ProviderDriverKind = DEFAULT_PROVIDER_DRIVER_KIND, ): string | null { - if (typeof model !== "string") { - return null; - } - - const trimmed = model.trim(); + const trimmed = normalizeCustomModelSlug(model); if (!trimmed) { return null; } @@ -252,6 +248,15 @@ export function normalizeModelSlug( return typeof aliased === "string" ? aliased : trimmed; } +/** Custom model identifiers are provider-owned, so only trim them; never expand aliases. */ +export function normalizeCustomModelSlug(model: string | null | undefined): string | null { + if (typeof model !== "string") { + return null; + } + + return model.trim() || null; +} + export function resolveSelectableModel( provider: ProviderDriverKind, value: string | null | undefined, From 0936fd271f866f5f58cce067865c8d0262b9ea20 Mon Sep 17 00:00:00 2001 From: Rhiz3K <33246262+Rhiz3K@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:22:03 +0200 Subject: [PATCH 38/39] fix(web): preview workspace images in the file panel (#3996) Co-authored-by: Rhiz3K Co-authored-by: Julius Marminge --- apps/web/src/assets/assetUrls.ts | 24 ++++++++- .../src/components/files/FilePreviewPanel.tsx | 52 ++++++++++++++++++- .../files/projectFilesQueryState.ts | 10 +++- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/apps/web/src/assets/assetUrls.ts b/apps/web/src/assets/assetUrls.ts index 673b093e333..701af3a79fc 100644 --- a/apps/web/src/assets/assetUrls.ts +++ b/apps/web/src/assets/assetUrls.ts @@ -9,7 +9,15 @@ import { usePreparedConnection } from "~/state/session"; export { resolveAssetUrl } from "@t3tools/client-runtime/state/assets"; -export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { +export type AssetUrlState = + | { readonly _tag: "Loading" } + | { readonly _tag: "Failure" } + | { readonly _tag: "Success"; readonly url: string }; + +export function useAssetUrlState( + environmentId: EnvironmentId, + resource: AssetResource, +): AssetUrlState { const preparedConnection = usePreparedConnection(environmentId); const result = useAtomValue( assetEnvironment.createUrl({ @@ -17,10 +25,22 @@ export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResourc input: { resource }, }), ); + if (result._tag === "Failure") { + return { _tag: "Failure" }; + } if (preparedConnection._tag === "None" || result._tag !== "Success") { + return { _tag: "Loading" }; + } + const url = resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); + return url === null ? { _tag: "Failure" } : { _tag: "Success", url }; +} + +export function useAssetUrl(environmentId: EnvironmentId, resource: AssetResource): string | null { + const result = useAssetUrlState(environmentId, resource); + if (result._tag !== "Success") { return null; } - return resolveAssetUrl(preparedConnection.value.httpBaseUrl, result.value.relativeUrl); + return result.url; } export function useAssetUrls( diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 8c430d5d2ab..6ddd38e9d25 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -4,6 +4,7 @@ import type { ResolvedKeybindingsConfig, ScopedThreadRef, } from "@t3tools/contracts"; +import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; import { Editor } from "@pierre/diffs/editor"; import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; @@ -16,6 +17,7 @@ import * as Schema from "effect/Schema"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isBrowserPreviewFile, openFileInPreview } from "~/browser/openFileInPreview"; +import { useAssetUrlState } from "~/assets/assetUrls"; import ChatMarkdown from "~/components/ChatMarkdown"; import { OpenInPicker } from "~/components/chat/OpenInPicker"; import { useClientSettings } from "~/hooks/useSettings"; @@ -113,6 +115,43 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = ` `; type FilePostRender = NonNullable["onPostRender"]>; +function WorkspaceImagePreview(props: { + readonly environmentId: EnvironmentId; + readonly threadRef: ScopedThreadRef; + readonly absolutePath: string; + readonly alt: string; +}) { + const assetUrl = useAssetUrlState(props.environmentId, { + _tag: "workspace-file", + threadId: props.threadRef.threadId, + path: props.absolutePath, + }); + const [failedUrl, setFailedUrl] = useState(null); + + if (assetUrl._tag === "Failure" || (assetUrl._tag === "Success" && failedUrl === assetUrl.url)) { + return ( +
+ Unable to load workspace image. +
+ ); + } + + return assetUrl._tag === "Success" ? ( +
+ {props.alt} setFailedUrl(assetUrl.url)} + /> +
+ ) : ( +
+ +
+ ); +} + function clampFileLine(contents: string, requestedLine: number): number { let lineCount = 1; for (let index = 0; index < contents.length; index += 1) { @@ -630,7 +669,8 @@ export default function FilePreviewPanel({ const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false, }); - const file = useProjectFileQuery(environmentId, cwd, relativePath); + const isImage = relativePath !== null && isWorkspaceImagePreviewPath(relativePath); + const file = useProjectFileQuery(environmentId, cwd, relativePath, !isImage); const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); const [markdownView, setMarkdownView] = useState<{ path: string | null; @@ -818,7 +858,15 @@ export default function FilePreviewPanel({ relativePath ? "flex" : "hidden", )} > - {relativePath && file.error && file.data === null ? ( + {relativePath && isImage && absolutePath ? ( + + ) : relativePath && file.error && file.data === null ? (
{file.error}
diff --git a/apps/web/src/components/files/projectFilesQueryState.ts b/apps/web/src/components/files/projectFilesQueryState.ts index 191b97d6a96..0d3fb8dd941 100644 --- a/apps/web/src/components/files/projectFilesQueryState.ts +++ b/apps/web/src/components/files/projectFilesQueryState.ts @@ -6,7 +6,7 @@ import type { } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; -import { AsyncResult } from "effect/unstable/reactivity"; +import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback } from "react"; import { appAtomRegistry } from "~/rpc/atomRegistry"; @@ -14,6 +14,9 @@ import { projectEnvironment } from "~/state/projects"; import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; const EMPTY_PROJECT_FILE_PATH = ""; +const EMPTY_PROJECT_FILE_QUERY_ATOM = Atom.make( + AsyncResult.initial(false), +).pipe(Atom.withLabel("project-file-query:empty")); function optimisticFileAtom(environmentId: EnvironmentId, cwd: string, relativePath: string) { return projectEnvironment.optimisticFile({ environmentId, cwd, relativePath }); } @@ -137,8 +140,11 @@ export function useProjectFileQuery( environmentId: EnvironmentId, cwd: string, relativePath: string | null, + enabled = true, ): ProjectQueryState { - const atom = getProjectFileQueryAtom(environmentId, cwd, relativePath); + const atom = enabled + ? getProjectFileQueryAtom(environmentId, cwd, relativePath) + : EMPTY_PROJECT_FILE_QUERY_ATOM; const result = useAtomValue(atom); const refreshAtom = useAtomRefresh(atom); const refresh = useCallback(() => refreshAtom(), [refreshAtom]); From 8ca4eec9ca46e1dd2f2427ae2405f5f417e8f95d Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 20 Jul 2026 08:26:46 -0400 Subject: [PATCH 39/39] feat(web): drag files from the explorer into the chat composer (#4140) Co-authored-by: Julius Marminge Signed-off-by: Yordis Prieto --- apps/web/src/components/chat/ChatComposer.tsx | 90 ++++++++++--- .../chat/composerMentionDrag.test.ts | 123 ++++++++++++++++++ .../components/chat/composerMentionDrag.ts | 98 ++++++++++++++ .../src/components/files/FileBrowserPanel.tsx | 44 +++++++ .../files/fileTreeDragMention.test.ts | 112 ++++++++++++++++ .../components/files/fileTreeDragMention.ts | 95 ++++++++++++++ 6 files changed, 542 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/components/chat/composerMentionDrag.test.ts create mode 100644 apps/web/src/components/chat/composerMentionDrag.ts create mode 100644 apps/web/src/components/files/fileTreeDragMention.test.ts create mode 100644 apps/web/src/components/files/fileTreeDragMention.ts diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 6ec2e631d19..f3346b4100a 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -44,6 +44,10 @@ import { shouldSubmitComposerOnEnter, } from "../../composer-logic"; import { deriveComposerSendState, readFileAsDataUrl } from "../ChatView.logic"; +import { + dataTransferHasComposerMention, + makeComposerMentionDragHandlers, +} from "./composerMentionDrag"; import { type ComposerImageAttachment, type DraftId, @@ -1878,6 +1882,67 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) addComposerImages(files); focusComposer(); }; + + const insertComposerTextAtEnd = ( + text: string, + options?: { ensureLeadingBoundary?: boolean }, + ): boolean => { + if ( + text.length === 0 || + isConnecting || + isComposerApprovalState || + pendingUserInputs.length > 0 || + projectSelectionRequired || + (environmentUnavailable !== null && activePendingProgress === null) + ) { + return false; + } + const prompt = promptRef.current; + const needsLeadingSpace = + (options?.ensureLeadingBoundary ?? false) && prompt.length > 0 && !/\s$/.test(prompt); + return applyPromptReplacement( + prompt.length, + prompt.length, + needsLeadingSpace ? ` ${text}` : text, + ); + }; + + // File-tree drags land as mentions. Handled in the capture phase so the + // editor never sees the drop; the load-bearing rules (native stop, "move" + // effect, no eager focus) live in makeComposerMentionDragHandlers. + const composerMentionDragHandlers = makeComposerMentionDragHandlers({ + insertMentionAtEnd: (text) => insertComposerTextAtEnd(text, { ensureLeadingBoundary: true }), + setDragActive: setIsDragOverComposer, + onInsertRejected: () => { + toastManager.add({ + type: "error", + title: "Unable to add to chat", + description: "The composer is busy; try again once it is ready.", + }); + }, + }); + + const onComposerMentionDragLeaveCapture = (event: React.DragEvent) => { + if (!dataTransferHasComposerMention(event.dataTransfer.types)) return; + event.stopPropagation(); + const nextTarget = event.relatedTarget; + if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) return; + setIsDragOverComposer(false); + }; + + // A cancelled drag (Escape) can end without a dragleave on the hovered + // target, which would leave the drop highlight stuck. dragend always fires + // on the in-page drag source and bubbles to window, so it is the reset of + // last resort while the highlight is up. + useEffect(() => { + if (!isDragOverComposer) return; + const onWindowDragEnd = () => { + dragDepthRef.current = 0; + setIsDragOverComposer(false); + }; + window.addEventListener("dragend", onWindowDragEnd); + return () => window.removeEventListener("dragend", onWindowDragEnd); + }, [isDragOverComposer]); const handleInterruptPrimaryAction = useCallback(() => { void onInterrupt(); }, [onInterrupt]); @@ -1941,26 +2006,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) focusAt: (cursor: number) => { composerEditorRef.current?.focusAt(cursor); }, - insertTextAtEnd: (text: string, options?: { ensureLeadingBoundary?: boolean }) => { - if ( - text.length === 0 || - isConnecting || - isComposerApprovalState || - pendingUserInputs.length > 0 || - projectSelectionRequired || - (environmentUnavailable !== null && activePendingProgress === null) - ) { - return false; - } - const prompt = promptRef.current; - const needsLeadingSpace = - (options?.ensureLeadingBoundary ?? false) && prompt.length > 0 && !/\s$/.test(prompt); - return applyPromptReplacement( - prompt.length, - prompt.length, - needsLeadingSpace ? ` ${text}` : text, - ); - }, + insertTextAtEnd: insertComposerTextAtEnd, openModelPicker: () => { setIsComposerModelPickerOpen(true); }, @@ -2087,6 +2133,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) onDragOver={onComposerDragOver} onDragLeave={onComposerDragLeave} onDrop={onComposerDrop} + onDragEnterCapture={composerMentionDragHandlers.onDragEnter} + onDragOverCapture={composerMentionDragHandlers.onDragOver} + onDragLeaveCapture={onComposerMentionDragLeaveCapture} + onDropCapture={composerMentionDragHandlers.onDrop} >
}) => { + const mention = options?.mention ?? "[index.md](docs/index.md)"; + const calls: Array = []; + const event = { + dataTransfer: { + types: options?.types ?? [COMPOSER_MENTION_DRAG_TYPE, "text/plain"], + getData: (format: string) => (format === COMPOSER_MENTION_DRAG_TYPE ? mention : ""), + dropEffect: "none", + }, + nativeEvent: { + stopPropagation: () => void calls.push("nativeStopPropagation"), + }, + preventDefault: () => void calls.push("preventDefault"), + stopPropagation: () => void calls.push("stopPropagation"), + }; + return { event, calls }; +}; + +const makeHost = (insertResult = true) => { + const log: Array = []; + const host: ComposerMentionDropHost = { + insertMentionAtEnd: (text) => { + log.push(`insert:${text}`); + return insertResult; + }, + setDragActive: (active) => void log.push(`active:${active}`), + onInsertRejected: () => void log.push("rejected"), + }; + return { host, log }; +}; + +describe("composerMentionFromTreePath", () => { + it("serializes a file path into a mention", () => { + expect(composerMentionFromTreePath("docs/index.md")).toBe("[index.md](docs/index.md)"); + }); + + it("strips the trailing slash directory rows carry", () => { + expect(composerMentionFromTreePath("docs/architecture/")).toBe( + "[architecture](docs/architecture)", + ); + }); + + it("rejects drags that carry no path", () => { + expect(composerMentionFromTreePath("")).toBeNull(); + expect(composerMentionFromTreePath("/")).toBeNull(); + }); +}); + +describe("dataTransferHasComposerMention", () => { + it("detects the mention payload among drag types", () => { + expect(dataTransferHasComposerMention([COMPOSER_MENTION_DRAG_TYPE, "text/plain"])).toBe(true); + expect(dataTransferHasComposerMention(["Files"])).toBe(false); + expect(dataTransferHasComposerMention([])).toBe(false); + }); +}); + +describe("makeComposerMentionDragHandlers", () => { + it("leaves drags without the mention payload alone", () => { + const { host, log } = makeHost(); + const handlers = makeComposerMentionDragHandlers(host); + const { event, calls } = makeDragEvent({ types: ["Files"] }); + handlers.onDragEnter(event); + handlers.onDragOver(event); + handlers.onDrop(event); + expect(calls).toEqual([]); + expect(log).toEqual([]); + }); + + it("stops the native event too, not just the synthetic one", () => { + // React's stopPropagation only halts synthetic dispatch; without the + // native stop, the editor's own DOM listeners process the drop and sync + // their stale state back over the inserted mention. + const { host } = makeHost(); + const handlers = makeComposerMentionDragHandlers(host); + const { event, calls } = makeDragEvent(); + handlers.onDrop(event); + expect(calls).toContain("preventDefault"); + expect(calls).toContain("stopPropagation"); + expect(calls).toContain("nativeStopPropagation"); + }); + + it('answers dragover with the "move" effect the tree allows', () => { + // Naming an effect outside the source's effectAllowed makes the browser + // cancel the drop without ever firing it. + const { host } = makeHost(); + const handlers = makeComposerMentionDragHandlers(host); + const { event } = makeDragEvent(); + handlers.onDragOver(event); + expect(event.dataTransfer.dropEffect).toBe("move"); + }); + + it("inserts the mention with its trailing space and clears the highlight", () => { + const { host, log } = makeHost(); + const handlers = makeComposerMentionDragHandlers(host); + handlers.onDragEnter(makeDragEvent().event); + handlers.onDrop(makeDragEvent().event); + expect(log).toEqual(["active:true", "active:false", "insert:[index.md](docs/index.md) "]); + }); + + it("reports a rejected insert instead of failing silently", () => { + const { host, log } = makeHost(false); + const handlers = makeComposerMentionDragHandlers(host); + handlers.onDrop(makeDragEvent().event); + expect(log).toContain("rejected"); + }); + + it("ignores a drop whose payload is empty", () => { + const { host, log } = makeHost(); + const handlers = makeComposerMentionDragHandlers(host); + handlers.onDrop(makeDragEvent({ mention: "" }).event); + expect(log).toEqual(["active:false"]); + }); +}); diff --git a/apps/web/src/components/chat/composerMentionDrag.ts b/apps/web/src/components/chat/composerMentionDrag.ts new file mode 100644 index 00000000000..43b1bfd800d --- /dev/null +++ b/apps/web/src/components/chat/composerMentionDrag.ts @@ -0,0 +1,98 @@ +import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger"; + +/** + * Drag payload type carrying a serialized composer mention. Set on drags that + * start in the workspace file tree so the composer can tell them apart from + * OS file drags and plain text selections. + */ +export const COMPOSER_MENTION_DRAG_TYPE = "application/x-t3code-composer-mention"; + +export function composerMentionFromTreePath(treePath: string): string | null { + const relativePath = treePath.replace(/\/+$/, ""); + if (relativePath.length === 0) { + return null; + } + return serializeComposerFileLink(relativePath); +} + +export function dataTransferHasComposerMention(types: ReadonlyArray): boolean { + return types.includes(COMPOSER_MENTION_DRAG_TYPE); +} + +export interface ComposerMentionDragTransfer { + readonly types: ReadonlyArray; + getData(format: string): string; + dropEffect: string; +} + +export interface ComposerMentionDragEvent { + readonly dataTransfer: ComposerMentionDragTransfer; + readonly nativeEvent: { stopPropagation(): void }; + preventDefault(): void; + stopPropagation(): void; +} + +/** + * What a mention drop is allowed to do to the composer. Deliberately narrow: + * there is no way to focus the editor from here. Focusing it synchronously + * during the drop makes the not-yet-reconciled editor sync its stale empty + * state back over the inserted mention; the insert path already focuses on + * the next frame, after the editor has caught up. + */ +export interface ComposerMentionDropHost { + insertMentionAtEnd(text: string): boolean; + setDragActive(active: boolean): void; + onInsertRejected(): void; +} + +export interface ComposerMentionDragHandlers { + onDragEnter(event: ComposerMentionDragEvent): void; + onDragOver(event: ComposerMentionDragEvent): void; + onDrop(event: ComposerMentionDragEvent): void; +} + +export function makeComposerMentionDragHandlers( + host: ComposerMentionDropHost, +): ComposerMentionDragHandlers { + // Claim the event for the composer: React's stopPropagation only halts the + // synthetic dispatch, so the native event must be stopped too or the + // editor's own DOM listeners still process the drag. + const claim = (event: ComposerMentionDragEvent): boolean => { + if (!dataTransferHasComposerMention(event.dataTransfer.types)) { + return false; + } + event.preventDefault(); + event.stopPropagation(); + event.nativeEvent.stopPropagation(); + return true; + }; + return { + onDragEnter(event) { + if (claim(event)) { + host.setDragActive(true); + } + }, + onDragOver(event) { + if (!claim(event)) { + return; + } + // The tree constrains its drags to effectAllowed "move"; naming any + // other effect makes the browser cancel the drop without firing it. + event.dataTransfer.dropEffect = "move"; + host.setDragActive(true); + }, + onDrop(event) { + if (!claim(event)) { + return; + } + host.setDragActive(false); + const mention = event.dataTransfer.getData(COMPOSER_MENTION_DRAG_TYPE); + if (mention.length === 0) { + return; + } + if (!host.insertMentionAtEnd(`${mention} `)) { + host.onInsertRejected(); + } + }, + }; +} diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index f95022c3424..3f53d65533d 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -16,6 +16,7 @@ import { cn } from "~/lib/utils"; import { readLocalApi } from "~/localApi"; import { T3_PIERRE_ICONS } from "~/pierre-icons"; +import { createFileTreeDragMentionController } from "./fileTreeDragMention"; import { useProjectEntriesQuery } from "./projectFilesQueryState"; interface FileBrowserPanelProps { @@ -137,6 +138,14 @@ export default function FileBrowserPanel({ showEntryContextMenuRef.current = showEntryContextMenu; }); + const treeModelRef = useRef["model"] | null>(null); + const dragMention = useMemo( + () => + createFileTreeDragMentionController({ + deselect: (path) => treeModelRef.current?.getItem(path)?.deselect(), + }), + [], + ); const { model } = useFileTree({ composition: { contextMenu: { @@ -146,12 +155,21 @@ export default function FileBrowserPanel({ }, }, }, + // Rows only need to be draggable so entries can be dropped into the chat + // composer; rearranging files inside the tree stays off. + dragAndDrop: { canDrop: () => false }, density: "compact", fileTreeSearchMode: "hide-non-matches", flattenEmptyDirectories: true, initialExpansion: 1, icons: T3_PIERRE_ICONS, onSelectionChange: (selectedPaths) => { + dragMention.handleSelectionChange(selectedPaths); + // Starting a drag selects the dragged row; that selection is a side + // effect of the gesture, not a request to open the file. + if (dragMention.isDragInProgress()) { + return; + } const selectedPath = selectedPaths.at(-1)?.replace(/\/$/, ""); if (selectedPath && entryKindsRef.current.get(selectedPath) === "file") { onOpenFile(selectedPath); @@ -174,8 +192,34 @@ export default function FileBrowserPanel({ [entries], ); + // Tag tree drags with the composer mention payload. The row is read from + // the composed event path (the tree's shadow root is open), so this does + // not depend on running after the tree's own dragstart handler; the drag + // data store is writable for every dragstart listener in the dispatch. + // The capture phase runs before the tree's own dragstart handler selects + // the dragged row, so the drag flag is up before that selection emits. + const panelRef = useRef(null); + useEffect(() => { + treeModelRef.current = model; + }, [model]); + useEffect(() => { + const panel = panelRef.current; + if (panel === null) { + return; + } + const handleDragStart = (event: DragEvent) => dragMention.handleDragStart(event); + const handleDragEnd = () => dragMention.handleDragEnd(); + panel.addEventListener("dragstart", handleDragStart, true); + panel.addEventListener("dragend", handleDragEnd); + return () => { + panel.removeEventListener("dragstart", handleDragStart, true); + panel.removeEventListener("dragend", handleDragEnd); + }; + }, [dragMention]); + return (
diff --git a/apps/web/src/components/files/fileTreeDragMention.test.ts b/apps/web/src/components/files/fileTreeDragMention.test.ts new file mode 100644 index 00000000000..812501ab5d1 --- /dev/null +++ b/apps/web/src/components/files/fileTreeDragMention.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { COMPOSER_MENTION_DRAG_TYPE } from "~/components/chat/composerMentionDrag"; +import { createFileTreeDragMentionController } from "./fileTreeDragMention.ts"; + +const makeTransfer = (plainText = "") => { + const data = new Map([["text/plain", plainText]]); + return { + setData: (format: string, value: string) => void data.set(format, value), + getData: (format: string) => data.get(format) ?? "", + data, + }; +}; + +const rowNode = (path: string) => ({ + getAttribute: (name: string) => (name === "data-item-path" ? path : null), +}); + +describe("createFileTreeDragMentionController", () => { + it("tags a row drag with the mention payload and flags the drag", () => { + const controller = createFileTreeDragMentionController({ deselect: () => {} }); + const transfer = makeTransfer(); + controller.handleDragStart({ + dataTransfer: transfer, + composedPath: () => [{}, rowNode("docs/index.md"), {}], + }); + expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[index.md](docs/index.md)"); + expect(controller.isDragInProgress()).toBe(true); + }); + + it("strips the trailing slash from directory rows", () => { + const controller = createFileTreeDragMentionController({ deselect: () => {} }); + const transfer = makeTransfer(); + controller.handleDragStart({ + dataTransfer: transfer, + composedPath: () => [rowNode("docs/architecture/")], + }); + expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[architecture](docs/architecture)"); + }); + + it("does not tag drags of selected text from the panel chrome", () => { + // Only a drag that originates on a tree row is a mention; dragging a text + // selection also carries text/plain, and tagging it would drop an invalid + // pill into the composer. + const controller = createFileTreeDragMentionController({ deselect: () => {} }); + const transfer = makeTransfer("selected text"); + controller.handleDragStart({ dataTransfer: transfer, composedPath: () => [{}] }); + expect(transfer.data.has(COMPOSER_MENTION_DRAG_TYPE)).toBe(false); + expect(controller.isDragInProgress()).toBe(false); + }); + + it("ignores drags that carry no row path", () => { + const controller = createFileTreeDragMentionController({ deselect: () => {} }); + const transfer = makeTransfer(); + controller.handleDragStart({ dataTransfer: transfer, composedPath: () => [{}] }); + expect(transfer.data.has(COMPOSER_MENTION_DRAG_TYPE)).toBe(false); + expect(controller.isDragInProgress()).toBe(false); + }); + + it("deselects the dragged row exactly once when the drag ends", () => { + const deselected: Array = []; + const controller = createFileTreeDragMentionController({ + deselect: (path) => deselected.push(path), + }); + controller.handleDragStart({ + dataTransfer: makeTransfer(), + composedPath: () => [rowNode("src/app.ts")], + }); + controller.handleDragEnd(); + controller.handleDragEnd(); + expect(deselected).toEqual(["src/app.ts"]); + expect(controller.isDragInProgress()).toBe(false); + }); + + it("drags the whole selection when the dragged row is part of it", () => { + const deselected: Array = []; + const controller = createFileTreeDragMentionController({ + deselect: (path) => deselected.push(path), + }); + controller.handleSelectionChange(["docs/index.md", "docs/api.md", "src/app.ts"]); + const transfer = makeTransfer(); + controller.handleDragStart({ + dataTransfer: transfer, + composedPath: () => [rowNode("docs/api.md")], + }); + expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe( + "[index.md](docs/index.md) [api.md](docs/api.md) [app.ts](src/app.ts)", + ); + controller.handleDragEnd(); + expect(deselected).toEqual(["docs/index.md", "docs/api.md", "src/app.ts"]); + }); + + it("drags only the row under the cursor when it is outside the selection", () => { + const controller = createFileTreeDragMentionController({ deselect: () => {} }); + controller.handleSelectionChange(["docs/index.md"]); + const transfer = makeTransfer(); + controller.handleDragStart({ + dataTransfer: transfer, + composedPath: () => [rowNode("src/app.ts")], + }); + expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[app.ts](src/app.ts)"); + }); + + it("does not deselect anything when no drag was started", () => { + const deselected: Array = []; + const controller = createFileTreeDragMentionController({ + deselect: (path) => deselected.push(path), + }); + controller.handleDragEnd(); + expect(deselected).toEqual([]); + }); +}); diff --git a/apps/web/src/components/files/fileTreeDragMention.ts b/apps/web/src/components/files/fileTreeDragMention.ts new file mode 100644 index 00000000000..7c17639a649 --- /dev/null +++ b/apps/web/src/components/files/fileTreeDragMention.ts @@ -0,0 +1,95 @@ +import { + COMPOSER_MENTION_DRAG_TYPE, + composerMentionFromTreePath, +} from "~/components/chat/composerMentionDrag"; + +interface FileTreeDragTransfer { + setData(format: string, data: string): void; +} + +export interface FileTreeDragStartEvent { + readonly dataTransfer: FileTreeDragTransfer | null; + composedPath(): ReadonlyArray; +} + +export interface FileTreeDragMentionHost { + /** Drop the tree's gesture-applied selection of the dragged row. */ + deselect(treePath: string): void; +} + +export interface FileTreeDragMentionController { + /** + * True from the moment a row drag starts until it ends. The tree selects + * the dragged row as part of the gesture; selection changes made while + * this is set are gesture side effects, not requests to open a file. + */ + isDragInProgress(): boolean; + /** Mirror of the tree's current selection, needed for multi-row drags. */ + handleSelectionChange(selectedPaths: ReadonlyArray): void; + handleDragStart(event: FileTreeDragStartEvent): void; + handleDragEnd(): void; +} + +const itemPathOf = (node: unknown): string | null => { + if (typeof node !== "object" || node === null) { + return null; + } + const element = node as { getAttribute?: (name: string) => string | null }; + return typeof element.getAttribute === "function" ? element.getAttribute("data-item-path") : null; +}; + +/** + * Tags file-tree drags with the composer mention payload and keeps the drag + * from acting like a click: while the drag runs, selection changes are + * suppressed, and when it ends the dragged rows are deselected so nothing is + * left highlighted and a later click on them still fires a selection change. + */ +export function createFileTreeDragMentionController( + host: FileTreeDragMentionHost, +): FileTreeDragMentionController { + let selection: ReadonlyArray = []; + let draggedPaths: ReadonlyArray = []; + return { + isDragInProgress: () => draggedPaths.length > 0, + handleSelectionChange(selectedPaths) { + selection = selectedPaths; + }, + handleDragStart(event) { + if (event.dataTransfer === null) { + return; + } + // Only drags that originate on a tree row are mentions; a text/plain + // fallback would also tag drags of selected text from the panel chrome. + let itemPath: string | null = null; + for (const node of event.composedPath()) { + itemPath = itemPathOf(node); + if (itemPath !== null) { + break; + } + } + if (itemPath === null) { + return; + } + // Same rule the tree applies to the drag itself: dragging a row that is + // part of the current selection drags the whole selection. + const dragged = selection.includes(itemPath) ? selection : [itemPath]; + const mentions = dragged + .map((path) => composerMentionFromTreePath(path)) + .filter((mention): mention is string => mention !== null); + if (mentions.length === 0) { + return; + } + draggedPaths = dragged; + event.dataTransfer.setData(COMPOSER_MENTION_DRAG_TYPE, mentions.join(" ")); + }, + handleDragEnd() { + if (draggedPaths.length === 0) { + return; + } + for (const path of draggedPaths) { + host.deselect(path); + } + draggedPaths = []; + }, + }; +}