From b511227b7ad421c422f1ebca65116776020e4799 Mon Sep 17 00:00:00 2001 From: maria Date: Sun, 19 Jul 2026 17:02:23 -0400 Subject: [PATCH 01/35] 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 02/35] 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 03/35] 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 04/35] 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 05/35] 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 06/35] 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 07/35] 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 08/35] 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 09/35] 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 10/35] =?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 11/35] 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 12/35] 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 13/35] 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 14/35] 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 15/35] 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 16/35] 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 17/35] 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 18/35] [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 19/35] 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 20/35] 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 21/35] 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 22/35] 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 23/35] 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 24/35] 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 25/35] 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 27/35] 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 28/35] [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 29/35] 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 30/35] 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 = []; + }, + }; +} From 2c199aacac3244adc93c5f4a9c838683e93946dd Mon Sep 17 00:00:00 2001 From: Anirudh Coontoor Date: Mon, 20 Jul 2026 18:13:22 +0530 Subject: [PATCH 31/35] fix(desktop): preserve main window bounds (#3851) Co-authored-by: Julius Marminge Co-authored-by: codex --- apps/desktop/src/app/DesktopLifecycle.ts | 8 +- .../src/backend/DesktopBackendPool.test.ts | 1 + .../src/backend/DesktopServerExposure.test.ts | 1 + .../src/settings/DesktopAppSettings.test.ts | 45 ++ .../src/settings/DesktopAppSettings.ts | 77 +++ .../src/updates/DesktopUpdates.test.ts | 1 + .../src/window/DesktopApplicationMenu.test.ts | 1 + apps/desktop/src/window/DesktopWindow.test.ts | 518 +++++++++++++++++- apps/desktop/src/window/DesktopWindow.ts | 173 +++++- apps/web/src/components/AppSidebarLayout.tsx | 40 +- .../src/components/threadSidebarWidth.test.ts | 34 ++ apps/web/src/components/threadSidebarWidth.ts | 22 + apps/web/src/components/ui/sidebar.tsx | 12 +- 13 files changed, 917 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/components/threadSidebarWidth.test.ts create mode 100644 apps/web/src/components/threadSidebarWidth.ts diff --git a/apps/desktop/src/app/DesktopLifecycle.ts b/apps/desktop/src/app/DesktopLifecycle.ts index c5264332b66..f8e05915718 100644 --- a/apps/desktop/src/app/DesktopLifecycle.ts +++ b/apps/desktop/src/app/DesktopLifecycle.ts @@ -73,8 +73,14 @@ function addScopedListener>( } const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")( - function* (): Effect.fn.Return { + function* (): Effect.fn.Return< + void, + never, + DesktopShutdown.DesktopShutdown | DesktopWindow.DesktopWindow + > { const shutdown = yield* DesktopShutdown.DesktopShutdown; + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.flushMainWindowBounds; yield* shutdown.request; yield* shutdown.awaitComplete; }, diff --git a/apps/desktop/src/backend/DesktopBackendPool.test.ts b/apps/desktop/src/backend/DesktopBackendPool.test.ts index 5e6a3f5164d..fa0811d5df7 100644 --- a/apps/desktop/src/backend/DesktopBackendPool.test.ts +++ b/apps/desktop/src/backend/DesktopBackendPool.test.ts @@ -76,6 +76,7 @@ function makePoolLayer( showConnectingSplash: Effect.void, handleBackendReady: () => Effect.void, handleBackendNotReady: Effect.void, + flushMainWindowBounds: Effect.void, dispatchMenuAction: () => Effect.die("unexpected menu action"), syncAppearance: Effect.void, } satisfies DesktopWindow.DesktopWindow["Service"]), diff --git a/apps/desktop/src/backend/DesktopServerExposure.test.ts b/apps/desktop/src/backend/DesktopServerExposure.test.ts index 1a107c5c856..dcfee93778d 100644 --- a/apps/desktop/src/backend/DesktopServerExposure.test.ts +++ b/apps/desktop/src/backend/DesktopServerExposure.test.ts @@ -250,6 +250,7 @@ describe("DesktopServerExposure", () => { const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), + setMainWindowBounds: () => Effect.die("unexpected main window bounds update"), setServerExposureMode: () => Effect.fail(settingsFailure), setTailscaleServe: () => Effect.fail(settingsFailure), setUpdateChannel: () => Effect.die("unexpected update channel change"), diff --git a/apps/desktop/src/settings/DesktopAppSettings.test.ts b/apps/desktop/src/settings/DesktopAppSettings.test.ts index 70b26798266..3878b0e36ad 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.test.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.test.ts @@ -11,6 +11,17 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as DesktopAppSettings from "./DesktopAppSettings.ts"; const DesktopSettingsPatch = Schema.Struct({ + mainWindowBounds: Schema.optionalKey( + Schema.NullOr( + Schema.Struct({ + x: Schema.Number, + y: Schema.Number, + width: Schema.Number, + height: Schema.Number, + }), + ), + ), + mainWindowMaximized: Schema.optionalKey(Schema.Boolean), serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])), tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean), tailscaleServePort: Schema.optionalKey(Schema.Number), @@ -91,6 +102,8 @@ describe("DesktopSettings", () => { assert.deepEqual( DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"), { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "local-only", tailscaleServeEnabled: false, tailscaleServePort: 443, @@ -116,6 +129,8 @@ describe("DesktopSettings", () => { }); assert.deepEqual(yield* settings.load, { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "network-accessible", tailscaleServeEnabled: true, tailscaleServePort: 8443, @@ -215,10 +230,13 @@ describe("DesktopSettings", () => { "serverExposureMode": "network-accessible", "tailscaleServeEnabled": true, "tailscaleServePort": 8443, + "mainWindowBounds": { "x": 120, "y": 80, "width": 1280, "height": 900 }, }\n`, ); assert.deepEqual(yield* settings.load, { + mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 }, + mainWindowMaximized: false, serverExposureMode: "network-accessible", tailscaleServeEnabled: true, tailscaleServePort: 8443, @@ -232,6 +250,24 @@ describe("DesktopSettings", () => { ), ); + it.effect("rejects window bounds that do not satisfy the domain schema", () => + withSettings( + Effect.gen(function* () { + const settings = yield* DesktopAppSettings.DesktopAppSettings; + yield* writeSettingsPatch({ + mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 }, + mainWindowMaximized: true, + serverExposureMode: "network-accessible", + }); + + const loaded = yield* settings.load; + assert.isNull(loaded.mainWindowBounds); + assert.isFalse(loaded.mainWindowMaximized); + assert.equal(loaded.serverExposureMode, "network-accessible"); + }), + ), + ); + it.effect("persists sparse desktop settings documents", () => withSettings( Effect.gen(function* () { @@ -239,12 +275,15 @@ describe("DesktopSettings", () => { const fileSystem = yield* FileSystem.FileSystem; const settings = yield* DesktopAppSettings.DesktopAppSettings; + yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 1440, height: 960 }, true); yield* settings.setServerExposureMode("network-accessible"); const persisted = yield* decodeDesktopSettingsPatch( yield* fileSystem.readFileString(environment.desktopSettingsPath), ); assert.deepEqual(persisted, { + mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 }, + mainWindowMaximized: true, serverExposureMode: "network-accessible", } satisfies typeof DesktopSettingsPatch.Type); }), @@ -261,6 +300,8 @@ describe("DesktopSettings", () => { }); assert.deepEqual(yield* settings.load, { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "local-only", tailscaleServeEnabled: false, tailscaleServePort: 443, @@ -286,6 +327,8 @@ describe("DesktopSettings", () => { }); assert.deepEqual(yield* settings.load, { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "local-only", tailscaleServeEnabled: false, tailscaleServePort: 443, @@ -310,6 +353,8 @@ describe("DesktopSettings", () => { }); assert.deepEqual(yield* settings.load, { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "local-only", tailscaleServeEnabled: true, tailscaleServePort: 443, diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 6a26bf5a6e2..466c9a9b5f8 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -20,6 +20,8 @@ import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts import { isValidDistroName } from "../wsl/wslPathParsing.ts"; export interface DesktopSettings { + readonly mainWindowBounds: DesktopWindowBounds | null; + readonly mainWindowMaximized: boolean; readonly serverExposureMode: DesktopServerExposureMode; readonly tailscaleServeEnabled: boolean; readonly tailscaleServePort: number; @@ -48,8 +50,25 @@ export interface DesktopSettingsChange { } export const DEFAULT_TAILSCALE_SERVE_PORT = 443; +const MIN_MAIN_WINDOW_SIZE = { + width: 840, + height: 620, +} as const; +export const DesktopWindowBoundsSchema = Schema.Struct({ + x: Schema.Int, + y: Schema.Int, + width: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.width)), + height: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.height)), +}); +export type DesktopWindowBounds = typeof DesktopWindowBoundsSchema.Type; +export const DEFAULT_MAIN_WINDOW_SIZE = { + width: 1100, + height: 780, +} as const; export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { + mainWindowBounds: null, + mainWindowMaximized: false, serverExposureMode: "local-only", tailscaleServeEnabled: false, tailscaleServePort: DEFAULT_TAILSCALE_SERVE_PORT, @@ -60,7 +79,16 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { wslOnly: false, }; +const DesktopWindowBoundsDocument = Schema.Struct({ + x: Schema.Number, + y: Schema.Number, + width: Schema.Number, + height: Schema.Number, +}); + const DesktopSettingsDocument = Schema.Struct({ + mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)), + mainWindowMaximized: Schema.optionalKey(Schema.Boolean), serverExposureMode: Schema.optionalKey(DesktopServerExposureModeSchema), tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean), tailscaleServePort: Schema.optionalKey(Schema.Number), @@ -81,6 +109,8 @@ type Mutable = { -readonly [K in keyof T]: T[K] }; const DesktopSettingsJson = fromLenientJson(DesktopSettingsDocument); const decodeDesktopSettingsJson = Schema.decodeEffect(DesktopSettingsJson); const encodeDesktopSettingsJson = Schema.encodeEffect(DesktopSettingsJson); +const decodeDesktopWindowBounds = Schema.decodeUnknownOption(DesktopWindowBoundsSchema); +const desktopWindowBoundsEquivalence = Schema.toEquivalence(DesktopWindowBoundsSchema); const settingsChange = (settings: DesktopSettings, changed: boolean): DesktopSettingsChange => ({ settings, @@ -114,6 +144,10 @@ export class DesktopAppSettings extends Context.Service< { readonly load: Effect.Effect; readonly get: Effect.Effect; + readonly setMainWindowBounds: ( + bounds: DesktopWindowBounds, + isMaximized: boolean, + ) => Effect.Effect; readonly setServerExposureMode: ( mode: DesktopServerExposureMode, ) => Effect.Effect; @@ -158,11 +192,16 @@ function normalizeWslDistro(value: unknown): string | null { return typeof value === "string" && isValidDistroName(value) ? value : null; } +export function normalizeMainWindowBounds(value: unknown): DesktopWindowBounds | null { + return Option.getOrNull(decodeDesktopWindowBounds(value)); +} + function normalizeDesktopSettingsDocument( parsed: DesktopSettingsDocument, appVersion: string, ): DesktopSettings { const defaultSettings = resolveDefaultDesktopSettings(appVersion); + const mainWindowBounds = normalizeMainWindowBounds(parsed.mainWindowBounds); const parsedUpdateChannel = Option.fromNullishOr(parsed.updateChannel); const isLegacySettings = parsed.updateChannelConfiguredByUser === undefined; const updateChannelConfiguredByUser = @@ -177,6 +216,8 @@ function normalizeDesktopSettingsDocument( (parsed.wslBackendEnabled === undefined && parsed.wslMode === "wsl"); return { + mainWindowBounds, + mainWindowMaximized: mainWindowBounds !== null && parsed.mainWindowMaximized === true, serverExposureMode: parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only", tailscaleServeEnabled: parsed.tailscaleServeEnabled === true, @@ -197,6 +238,12 @@ function toDesktopSettingsDocument( ): DesktopSettingsDocument { const document: Mutable = {}; + if (settings.mainWindowBounds !== null) { + document.mainWindowBounds = settings.mainWindowBounds; + } + if (settings.mainWindowMaximized) { + document.mainWindowMaximized = true; + } if (settings.serverExposureMode !== defaults.serverExposureMode) { document.serverExposureMode = settings.serverExposureMode; } @@ -237,6 +284,22 @@ function setServerExposureMode( }; } +function setMainWindowBounds( + settings: DesktopSettings, + bounds: DesktopWindowBounds, + isMaximized: boolean, +): DesktopSettings { + return settings.mainWindowBounds !== null && + desktopWindowBoundsEquivalence(settings.mainWindowBounds, bounds) && + settings.mainWindowMaximized === isMaximized + ? settings + : { + ...settings, + mainWindowBounds: bounds, + mainWindowMaximized: isMaximized, + }; +} + function setTailscaleServe( settings: DesktopSettings, input: { readonly enabled: boolean; readonly port: Option.Option }, @@ -431,6 +494,18 @@ export const make = Effect.gen(function* () { ); return yield* SynchronizedRef.setAndGet(settingsRef, settings); }).pipe(Effect.withSpan("desktop.settings.load")), + setMainWindowBounds: (bounds, isMaximized) => + persist((settings) => setMainWindowBounds(settings, bounds, isMaximized)).pipe( + Effect.withSpan("desktop.settings.setMainWindowBounds", { + attributes: { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + isMaximized, + }, + }), + ), setServerExposureMode: (mode) => persist((settings) => setServerExposureMode(settings, mode)).pipe( Effect.withSpan("desktop.settings.setServerExposureMode", { attributes: { mode } }), @@ -488,6 +563,8 @@ export const layerTest = (initialSettings: DesktopSettings = DEFAULT_DESKTOP_SET return DesktopAppSettings.of({ get: SynchronizedRef.get(settingsRef), load: SynchronizedRef.get(settingsRef), + setMainWindowBounds: (bounds, isMaximized) => + update((settings) => setMainWindowBounds(settings, bounds, isMaximized)), setServerExposureMode: (mode) => update((settings) => setServerExposureMode(settings, mode)), setTailscaleServe: (input) => update((settings) => setTailscaleServe(settings, input)), diff --git a/apps/desktop/src/updates/DesktopUpdates.test.ts b/apps/desktop/src/updates/DesktopUpdates.test.ts index 5ae92bbee96..32224c7a5ca 100644 --- a/apps/desktop/src/updates/DesktopUpdates.test.ts +++ b/apps/desktop/src/updates/DesktopUpdates.test.ts @@ -158,6 +158,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { ? Layer.succeed(DesktopAppSettings.DesktopAppSettings, { get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS), + setMainWindowBounds: () => Effect.die("unexpected main window bounds update"), setServerExposureMode: () => Effect.die("unexpected server exposure update"), setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"), setUpdateChannel: () => Effect.fail(setUpdateChannelError), diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index ba77292fdc8..168846466ed 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.test.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.test.ts @@ -76,6 +76,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred) => showConnectingSplash: Effect.void, handleBackendReady: () => Effect.void, handleBackendNotReady: Effect.void, + flushMainWindowBounds: Effect.void, dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid), syncAppearance: Effect.void, } satisfies DesktopWindow.DesktopWindow["Service"]); diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 492ef1ad577..587da8d4431 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -1,12 +1,17 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Logger from "effect/Logger"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; +import * as References from "effect/References"; +import * as Schema from "effect/Schema"; import * as TestClock from "effect/testing/TestClock"; -import type * as Electron from "electron"; +import * as Electron from "electron"; import { vi } from "vite-plus/test"; vi.mock("electron", async (importOriginal) => ({ @@ -18,12 +23,20 @@ vi.mock("electron", async (importOriginal) => ({ setUserAgent: vi.fn(), })), }, + screen: { + getAllDisplays: vi.fn(() => [ + { + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + }, + ]), + }, })); import * as DesktopAssets from "../app/DesktopAssets.ts"; import * as DesktopConfig from "../app/DesktopConfig.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as DesktopState from "../app/DesktopState.ts"; +import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as ElectronMenu from "../electron/ElectronMenu.ts"; import * as ElectronShell from "../electron/ElectronShell.ts"; import * as ElectronTheme from "../electron/ElectronTheme.ts"; @@ -66,15 +79,21 @@ function makeFakeBrowserWindow() { const window = { close: vi.fn(), focus: vi.fn(), + getBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })), + getNormalBounds: vi.fn(() => ({ x: 0, y: 0, width: 1100, height: 780 })), isDestroyed: vi.fn(() => false), isFullScreen: vi.fn(() => false), + isMaximized: vi.fn(() => false), isMinimized: vi.fn(() => false), isVisible: vi.fn(() => true), loadURL: vi.fn(() => Promise.resolve()), + maximize: vi.fn(), on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => { windowListeners.set(eventName, listener); }), - once: vi.fn(), + once: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => { + windowListeners.set(eventName, listener); + }), restore: vi.fn(), setBackgroundColor: vi.fn(), setAutoHideCursor: vi.fn(), @@ -86,7 +105,14 @@ function makeFakeBrowserWindow() { return { window: window as unknown as Electron.BrowserWindow, + getBounds: window.getBounds, + getNormalBounds: window.getNormalBounds, + isDestroyed: window.isDestroyed, + isFullScreen: window.isFullScreen, + isMaximized: window.isMaximized, + isMinimized: window.isMinimized, loadURL: window.loadURL, + maximize: window.maximize, openDevTools: webContents.openDevTools, reload: webContents.reload, send: webContents.send, @@ -144,13 +170,57 @@ const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe( ), ); +const desktopWindowBoundsEquivalence = Schema.toEquivalence( + DesktopAppSettings.DesktopWindowBoundsSchema, +); + function makeTestLayer(input: { readonly window: Electron.BrowserWindow; readonly createCount: Ref.Ref; readonly mainWindow: Ref.Ref>; readonly createdWindowOptions?: Electron.BrowserWindowConstructorOptions[]; + readonly desktopSettings?: DesktopAppSettings.DesktopSettings; + readonly mainWindowBoundsUpdates?: DesktopAppSettings.DesktopWindowBounds[]; + readonly mainWindowMaximizedUpdates?: boolean[]; + readonly beforeMainWindowBoundsUpdate?: ( + bounds: DesktopAppSettings.DesktopWindowBounds, + ) => Effect.Effect; readonly openedExternalUrls?: unknown[]; }) { + let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS; + const desktopAppSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, { + get: Effect.sync(() => desktopSettings), + load: Effect.sync(() => desktopSettings), + setMainWindowBounds: (bounds, isMaximized) => + Effect.gen(function* () { + if (input.beforeMainWindowBoundsUpdate) { + yield* input.beforeMainWindowBoundsUpdate(bounds); + } + const changed = + desktopSettings.mainWindowBounds === null || + !desktopWindowBoundsEquivalence(desktopSettings.mainWindowBounds, bounds) || + desktopSettings.mainWindowMaximized !== isMaximized; + if (changed) { + desktopSettings = { + ...desktopSettings, + mainWindowBounds: bounds, + mainWindowMaximized: isMaximized, + }; + input.mainWindowBoundsUpdates?.push(bounds); + input.mainWindowMaximizedUpdates?.push(isMaximized); + } + return { settings: desktopSettings, changed }; + }), + setServerExposureMode: () => Effect.die("unexpected server exposure update"), + setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"), + setUpdateChannel: () => Effect.die("unexpected update channel change"), + setWslBackendEnabled: () => Effect.die("unexpected WSL backend toggle"), + setWslDistro: () => Effect.die("unexpected WSL distro change"), + setWslOnly: () => Effect.die("unexpected WSL-only toggle"), + applyWslWindowsFallback: Effect.die("unexpected WSL Windows fallback"), + applyWslWindowsFallbackInMemory: Effect.die("unexpected WSL Windows fallback"), + } satisfies DesktopAppSettings.DesktopAppSettings["Service"]); + const electronWindowLayer = Layer.succeed(ElectronWindow.ElectronWindow, { create: (options) => Effect.sync(() => { @@ -175,6 +245,7 @@ function makeTestLayer(input: { Layer.mergeAll( desktopAssetsLayer, desktopEnvironmentLayer, + desktopAppSettingsLayer, desktopServerExposureLayer, DesktopState.layer, electronMenuLayer, @@ -272,6 +343,7 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n Layer.mergeAll( desktopAssetsLayer, desktopEnvironmentLayer, + DesktopAppSettings.layerTest(), desktopServerExposureLayer, electronMenuLayer, Layer.succeed(ElectronShell.ElectronShell, { @@ -294,6 +366,23 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n }); describe("DesktopWindow", () => { + it("restores bounds only when the window fits within a connected display", () => { + const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 }; + const displays = [ + { x: 0, y: 0, width: 1920, height: 1080 }, + { x: 1920, y: 0, width: 2560, height: 1440 }, + ]; + + assert.deepEqual( + DesktopWindow.resolveInitialMainWindowBounds(persistedBounds, displays), + persistedBounds, + ); + assert.deepEqual( + DesktopWindow.resolveInitialMainWindowBounds(persistedBounds, [displays[0]!]), + DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE, + ); + }); + it("recognizes only same-origin renderer navigations", () => { assert.isTrue( DesktopWindow.isSameOriginRendererNavigation({ @@ -335,6 +424,10 @@ describe("DesktopWindow", () => { yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); assert.equal(yield* Ref.get(createCount), 1); + assert.equal(createdWindowOptions[0]?.width, 1100); + assert.equal(createdWindowOptions[0]?.height, 780); + assert.isUndefined(createdWindowOptions[0]?.x); + assert.isUndefined(createdWindowOptions[0]?.y); assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor); assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]); assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]); @@ -343,6 +436,427 @@ describe("DesktopWindow", () => { }), ); + it.effect("uses the persisted main window bounds when opening the window", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + createdWindowOptions, + desktopSettings: { + ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, + mainWindowBounds: { x: 120, y: 80, width: 1320, height: 880 }, + }, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + assert.equal(createdWindowOptions[0]?.width, 1320); + assert.equal(createdWindowOptions[0]?.height, 880); + assert.equal(createdWindowOptions[0]?.x, 120); + assert.equal(createdWindowOptions[0]?.y, 80); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("restores the persisted maximized state", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + desktopSettings: { + ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, + mainWindowBounds: { x: 120, y: 80, width: 1320, height: 880 }, + mainWindowMaximized: true, + }, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + assert.equal(fakeWindow.maximize.mock.calls.length, 0); + const readyToShow = fakeWindow.windowListeners.get("ready-to-show"); + if (!readyToShow) { + return yield* Effect.die("window ready-to-show listener was not registered"); + } + readyToShow(); + assert.equal(fakeWindow.maximize.mock.calls.length, 1); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("debounces move and resize bounds updates", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const move = fakeWindow.windowListeners.get("move"); + const resize = fakeWindow.windowListeners.get("resize"); + if (!move || !resize) { + return yield* Effect.die("window bounds listeners were not registered"); + } + + fakeWindow.getBounds.mockReturnValue({ x: 120, y: 80, width: 1280, height: 840 }); + move(); + yield* TestClock.adjust(250); + + fakeWindow.getBounds.mockReturnValue({ x: 160, y: 100, width: 1360, height: 900 }); + resize(); + yield* TestClock.adjust(499); + assert.deepEqual(mainWindowBoundsUpdates, []); + + yield* TestClock.adjust(1); + yield* Effect.promise(() => Promise.resolve()); + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 160, y: 100, width: 1360, height: 900 }]); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("persists normal bounds and state for a maximized window", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.isMaximized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 }); + fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const close = fakeWindow.windowListeners.get("close"); + if (!close) { + return yield* Effect.die("window close listener was not registered"); + } + close(); + yield* Effect.promise(() => Promise.resolve()); + + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 220, y: 140, width: 1380, height: 920 }]); + assert.deepEqual(mainWindowMaximizedUpdates, [true]); + assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1); + assert.equal(fakeWindow.getBounds.mock.calls.length, 0); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("persists normal bounds and state from the native maximize event", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const mainWindowMaximizedUpdates: boolean[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + mainWindowMaximizedUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const maximize = fakeWindow.windowListeners.get("maximize"); + if (!maximize) { + return yield* Effect.die("window maximize listener was not registered"); + } + + fakeWindow.isMaximized.mockReturnValue(true); + fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 }); + fakeWindow.getNormalBounds.mockReturnValue({ x: 220, y: 140, width: 1380, height: 920 }); + maximize(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); + + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 220, y: 140, width: 1380, height: 920 }]); + assert.deepEqual(mainWindowMaximizedUpdates, [true]); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("does not persist bounds that fail the domain schema", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.getBounds.mockReturnValue({ x: 100.4, y: 80.2, width: 839.4, height: 619.4 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const resize = fakeWindow.windowListeners.get("resize"); + if (!resize) { + return yield* Effect.die("window resize listener was not registered"); + } + resize(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); + + assert.deepEqual(mainWindowBoundsUpdates, []); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("preserves unrestorable bounds until the user changes the window", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + desktopSettings: { + ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS, + mainWindowBounds: { x: 2040, y: 80, width: 1320, height: 880 }, + }, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const close = fakeWindow.windowListeners.get("close"); + const move = fakeWindow.windowListeners.get("move"); + if (!close || !move) { + return yield* Effect.die("window lifecycle listeners were not registered"); + } + + close(); + yield* Effect.promise(() => Promise.resolve()); + assert.deepEqual(mainWindowBoundsUpdates, []); + + fakeWindow.getBounds.mockReturnValue({ x: 80, y: 60, width: 1280, height: 840 }); + move(); + yield* TestClock.adjust(500); + yield* Effect.promise(() => Promise.resolve()); + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 80, y: 60, width: 1280, height: 840 }]); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("flushes normal bounds when fullscreen before the debounce completes", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.getBounds.mockReturnValue({ x: 0, y: 0, width: 1920, height: 1080 }); + fakeWindow.getNormalBounds.mockReturnValue({ x: 200, y: 130, width: 1400, height: 940 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const resize = fakeWindow.windowListeners.get("resize"); + if (!resize) { + return yield* Effect.die("window resize listener was not registered"); + } + resize(); + yield* TestClock.adjust(250); + fakeWindow.isFullScreen.mockReturnValue(true); + + yield* desktopWindow.flushMainWindowBounds; + + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 200, y: 130, width: 1400, height: 940 }]); + assert.equal(fakeWindow.getBounds.mock.calls.length, 0); + assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("flushes normal bounds when minimized before the debounce completes", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.getBounds.mockReturnValue({ x: -32_000, y: -32_000, width: 160, height: 28 }); + fakeWindow.getNormalBounds.mockReturnValue({ x: 180, y: 120, width: 1440, height: 960 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const resize = fakeWindow.windowListeners.get("resize"); + if (!resize) { + return yield* Effect.die("window resize listener was not registered"); + } + resize(); + yield* TestClock.adjust(250); + fakeWindow.isMinimized.mockReturnValue(true); + + yield* desktopWindow.flushMainWindowBounds; + + assert.deepEqual(mainWindowBoundsUpdates, [{ x: 180, y: 120, width: 1440, height: 960 }]); + assert.equal(fakeWindow.getBounds.mock.calls.length, 0); + assert.equal(fakeWindow.getNormalBounds.mock.calls.length, 1); + }).pipe(Effect.provide(layer)); + }), + ); + + it.effect("logs display lookup failures before falling back to the default size", () => + Effect.gen(function* () { + const displayLookupFailure = new Error("screen API unavailable"); + vi.mocked(Electron.screen.getAllDisplays).mockImplementationOnce(() => { + throw displayLookupFailure; + }); + const logRecords: Array<{ + readonly message: unknown; + readonly annotations: Readonly>; + }> = []; + const logger = Logger.make(({ fiber, message }) => { + logRecords.push({ + message, + annotations: { ...fiber.getRef(References.CurrentLogAnnotations) }, + }); + }); + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const createdWindowOptions: Electron.BrowserWindowConstructorOptions[] = []; + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + createdWindowOptions, + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + }).pipe( + Effect.provide(Layer.mergeAll(layer, Logger.layer([logger], { mergeWithExisting: false }))), + ); + + const warning = logRecords.find( + (record) => + Array.isArray(record.message) && + record.message[0] === "failed to read connected displays; using defaults", + ); + assert.isDefined(warning); + assert.strictEqual(warning.annotations.cause, displayLookupFailure); + assert.equal(createdWindowOptions[0]?.width, 1100); + assert.equal(createdWindowOptions[0]?.height, 780); + assert.isUndefined(createdWindowOptions[0]?.x); + assert.isUndefined(createdWindowOptions[0]?.y); + }), + ); + + it.effect("persists the current main window bounds before the window closes", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + fakeWindow.getBounds.mockReturnValue({ x: 240, y: 160, width: 1410, height: 930 }); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const mainWindowBoundsUpdates: DesktopAppSettings.DesktopWindowBounds[] = []; + const writeStarted = yield* Deferred.make(); + const allowWrite = yield* Deferred.make(); + const flushCompleted = yield* Deferred.make(); + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + mainWindowBoundsUpdates, + beforeMainWindowBoundsUpdate: () => + Deferred.succeed(writeStarted, undefined).pipe( + Effect.andThen(Deferred.await(allowWrite)), + Effect.asVoid, + ), + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773")); + + const close = fakeWindow.windowListeners.get("close"); + if (!close) { + return yield* Effect.die("window close listener was not registered"); + } + close(); + yield* Deferred.await(writeStarted); + fakeWindow.isDestroyed.mockReturnValue(true); + + const flushFiber = yield* desktopWindow.flushMainWindowBounds.pipe( + Effect.andThen(Deferred.succeed(flushCompleted, undefined)), + Effect.forkChild({ startImmediately: true }), + ); + yield* Effect.yieldNow; + assert.isFalse(yield* Deferred.isDone(flushCompleted)); + + yield* Deferred.succeed(allowWrite, undefined); + yield* Fiber.join(flushFiber); + assert.isTrue(yield* Deferred.isDone(flushCompleted)); + + assert.deepEqual(mainWindowBoundsUpdates, [ + { + x: 240, + y: 160, + width: 1410, + height: 930, + }, + ]); + }).pipe(Effect.provide(layer)); + }), + ); + it.effect("publishes native macOS fullscreen changes to the renderer", () => Effect.gen(function* () { const fakeWindow = makeFakeBrowserWindow(); diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index c808c77e51c..db4b698434d 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -5,7 +5,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; -import type * as Electron from "electron"; +import * as Electron from "electron"; import * as DesktopAssets from "../app/DesktopAssets.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; @@ -17,11 +17,13 @@ import * as ElectronTheme from "../electron/ElectronTheme.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; import { MENU_ACTION_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL } from "../ipc/channels.ts"; import * as PreviewManager from "../preview/Manager.ts"; +import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; const TITLEBAR_HEIGHT = 40; const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; +const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500; const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const; const DEVELOPMENT_RETRYABLE_LOAD_ERROR_CODES = new Set([ -2, // ERR_FAILED @@ -41,6 +43,7 @@ type WindowTitleBarOptions = Pick< type DesktopWindowRuntimeServices = | DesktopEnvironment.DesktopEnvironment | DesktopAssets.DesktopAssets + | DesktopAppSettings.DesktopAppSettings | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme @@ -75,6 +78,7 @@ export class DesktopWindow extends Context.Service< // window so a "macOS dock click" while the backend is down doesn't // produce a stranded window pointing at nothing. readonly handleBackendNotReady: Effect.Effect; + readonly flushMainWindowBounds: Effect.Effect; readonly dispatchMenuAction: (action: string) => Effect.Effect; readonly syncAppearance: Effect.Effect; } @@ -99,6 +103,45 @@ function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { return shouldUseDarkColors ? "#0a0a0a" : "#ffffff"; } +type DisplayBounds = Pick; + +function windowFitsWithinDisplay( + windowBounds: DesktopAppSettings.DesktopWindowBounds, + displayBounds: DisplayBounds, +): boolean { + return ( + windowBounds.x >= displayBounds.x && + windowBounds.y >= displayBounds.y && + windowBounds.x + windowBounds.width <= displayBounds.x + displayBounds.width && + windowBounds.y + windowBounds.height <= displayBounds.y + displayBounds.height + ); +} + +function windowBoundsEqual( + left: DesktopAppSettings.DesktopWindowBounds, + right: DesktopAppSettings.DesktopWindowBounds, +): boolean { + return ( + left.x === right.x && + left.y === right.y && + left.width === right.width && + left.height === right.height + ); +} + +export function resolveInitialMainWindowBounds( + persistedBounds: DesktopAppSettings.DesktopWindowBounds | null, + displays: readonly DisplayBounds[], +): DesktopAppSettings.DesktopWindowBounds | typeof DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE { + if ( + persistedBounds !== null && + displays.some((display) => windowFitsWithinDisplay(persistedBounds, display)) + ) { + return persistedBounds; + } + return DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE; +} + // A self-contained "Connecting to WSL" splash, shown immediately in wsl-only // mode while the WSL backend (which serves the renderer) cold-boots. Inlined as // a data URL so it needs no bundled asset and no backend — pure CSS, no JS. @@ -202,6 +245,7 @@ export const make = Effect.gen(function* () { const electronTheme = yield* ElectronTheme.ElectronTheme; const electronWindow = yield* ElectronWindow.ElectronWindow; const previewManager = yield* PreviewManager.PreviewManager; + const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; // Window-side latch for the primary backend's readiness. Set by // handleBackendReady (driven by the pool's onReady callback), cleared // by handleBackendNotReady (driven by onShutdown). Only consumed by @@ -214,6 +258,7 @@ export const make = Effect.gen(function* () { const context = yield* Effect.context(); const runFork = Effect.runForkWith(context); const runPromise = Effect.runPromiseWith(context); + let flushMainWindowBounds: Effect.Effect = Effect.void; const dismissConnectingSplash = Effect.gen(function* () { const splash = yield* Ref.getAndSet(splashWindowRef, Option.none()); @@ -250,9 +295,31 @@ export const make = Effect.gen(function* () { const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; + const persistedSettings = yield* desktopSettings.get; + const persistedBounds = persistedSettings.mainWindowBounds; + const displayBoundsResult = yield* Effect.sync(() => { + try { + return { + _tag: "Success" as const, + bounds: Electron.screen.getAllDisplays().map((display) => display.bounds), + }; + } catch (cause) { + return { _tag: "Failure" as const, cause }; + } + }); + const displayBounds = + displayBoundsResult._tag === "Success" + ? displayBoundsResult.bounds + : yield* logWindowWarning("failed to read connected displays; using defaults", { + cause: displayBoundsResult.cause, + }).pipe(Effect.as([])); + const initialBounds = resolveInitialMainWindowBounds(persistedBounds, displayBounds); + const restoredPersistedBounds = persistedBounds !== null && initialBounds === persistedBounds; + if (persistedBounds !== null && initialBounds === DesktopAppSettings.DEFAULT_MAIN_WINDOW_SIZE) { + yield* logWindowWarning("saved main window bounds could not be restored; using defaults"); + } const window = yield* electronWindow.create({ - width: 1100, - height: 780, + ...initialBounds, minWidth: 840, minHeight: 620, show: false, @@ -274,6 +341,92 @@ export const make = Effect.gen(function* () { if (environment.platform === "darwin") { window.setAutoHideCursor(false); } + let boundsPersistFiber: Fiber.Fiber | undefined; + let pendingBoundsPersistFiber: Fiber.Fiber | undefined; + let boundsPersistenceEnabled = persistedBounds === null || restoredPersistedBounds; + const readPersistableBounds = (): DesktopAppSettings.DesktopWindowBounds | null => { + if (window.isDestroyed()) { + return null; + } + const bounds = + window.isFullScreen() || window.isMaximized() || window.isMinimized() + ? window.getNormalBounds() + : window.getBounds(); + return DesktopAppSettings.normalizeMainWindowBounds({ + x: Math.round(bounds.x), + y: Math.round(bounds.y), + width: Math.round(bounds.width), + height: Math.round(bounds.height), + }); + }; + const fallbackWindowBounds = boundsPersistenceEnabled ? null : readPersistableBounds(); + const fallbackWindowMaximized = persistedSettings.mainWindowMaximized; + const persistCurrentBounds = (): Fiber.Fiber | undefined => { + if (!boundsPersistenceEnabled) { + return pendingBoundsPersistFiber; + } + const bounds = readPersistableBounds(); + if (bounds === null) { + return pendingBoundsPersistFiber; + } + pendingBoundsPersistFiber = runFork( + desktopSettings.setMainWindowBounds(bounds, window.isMaximized()).pipe( + Effect.asVoid, + Effect.catch((error) => + logWindowWarning("failed to persist main window bounds", { + message: error.message, + }), + ), + ), + ); + return pendingBoundsPersistFiber; + }; + const scheduleBoundsPersist = () => { + if (!boundsPersistenceEnabled) { + const currentBounds = readPersistableBounds(); + if ( + currentBounds === null || + (fallbackWindowBounds !== null && + windowBoundsEqual(currentBounds, fallbackWindowBounds) && + window.isMaximized() === fallbackWindowMaximized) + ) { + return; + } + } + boundsPersistenceEnabled = true; + if (boundsPersistFiber !== undefined) { + const fiber = boundsPersistFiber; + boundsPersistFiber = undefined; + runFork(Fiber.interrupt(fiber)); + } + boundsPersistFiber = runFork( + Effect.sleep(MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS).pipe( + Effect.andThen( + Effect.sync(() => { + boundsPersistFiber = undefined; + void persistCurrentBounds(); + }), + ), + ), + ); + }; + const clearBoundsPersist = () => { + if (boundsPersistFiber === undefined) { + return; + } + const fiber = boundsPersistFiber; + boundsPersistFiber = undefined; + runFork(Fiber.interrupt(fiber)); + }; + const flushBoundsPersist = Effect.sync(() => { + clearBoundsPersist(); + return persistCurrentBounds(); + }).pipe( + Effect.flatMap((fiber) => + fiber === undefined ? Effect.void : Fiber.join(fiber).pipe(Effect.asVoid), + ), + ); + flushMainWindowBounds = flushBoundsPersist; yield* previewManager.setMainWindow(window); window.webContents.on("will-attach-webview", (event, webPreferences, params) => { @@ -364,6 +517,13 @@ export const make = Effect.gen(function* () { event.preventDefault(); window.setTitle(environment.displayName); }); + window.on("resize", scheduleBoundsPersist); + window.on("move", scheduleBoundsPersist); + window.on("maximize", scheduleBoundsPersist); + window.on("unmaximize", scheduleBoundsPersist); + window.on("close", () => { + runFork(flushBoundsPersist); + }); if (environment.platform === "darwin") { window.on("enter-full-screen", () => { @@ -472,6 +632,9 @@ export const make = Effect.gen(function* () { bindFirstRevealTrigger(revealSubscribers, () => { // Reveal the real window, then close the connecting splash (if any) so the // two don't overlap and there's no blank gap between them. + if (persistedSettings.mainWindowMaximized) { + window.maximize(); + } void runPromise(Effect.andThen(electronWindow.reveal(window), dismissConnectingSplash)); }); @@ -482,6 +645,7 @@ export const make = Effect.gen(function* () { window.on("closed", () => { clearDevelopmentLoadRetry(); + clearBoundsPersist(); void runPromise(electronWindow.clearMain(Option.some(window))); }); @@ -598,6 +762,9 @@ export const make = Effect.gen(function* () { handleBackendNotReady: Ref.set(backendReadyRef, false).pipe( Effect.withSpan("desktop.window.handleBackendNotReady"), ), + flushMainWindowBounds: Effect.suspend(() => flushMainWindowBounds).pipe( + Effect.withSpan("desktop.window.flushMainWindowBounds"), + ), dispatchMenuAction: Effect.fn("desktop.window.dispatchMenuAction")(function* (action) { yield* Effect.annotateCurrentSpan({ action }); const existingWindow = yield* focusedMainWindow; diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index ee56858bc58..6c692dc3de8 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -1,13 +1,22 @@ import { useAtomValue } from "@effect/atom-react"; +import * as Schema from "effect/Schema"; import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { isElectron } from "../env"; +import { getLocalStorageItem } from "../hooks/useLocalStorage"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; import ThreadSidebar from "./Sidebar"; import { useSidebarStageBackdropVariant } from "./SidebarStageBackdrop"; +import { + resolveInitialThreadSidebarWidth, + resolveThreadSidebarMaximumWidth, + THREAD_MAIN_CONTENT_MIN_WIDTH, + THREAD_SIDEBAR_MIN_WIDTH, + THREAD_SIDEBAR_WIDTH_STORAGE_KEY, +} from "./threadSidebarWidth"; import { Sidebar, SidebarProvider, @@ -18,11 +27,20 @@ import { } from "./ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; -const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; -const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; -const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px"; +function readInitialThreadSidebarWidth(): number { + try { + return resolveInitialThreadSidebarWidth( + getLocalStorageItem(THREAD_SIDEBAR_WIDTH_STORAGE_KEY, Schema.Finite), + window.innerWidth, + ); + } catch (error) { + console.error("Could not read persisted thread sidebar width.", error); + return resolveInitialThreadSidebarWidth(null, window.innerWidth); + } +} + function SidebarControl() { const keybindings = useAtomValue(primaryServerKeybindingsAtom); const { toggleSidebar } = useSidebar(); @@ -82,16 +100,20 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { const navigate = useNavigate(); const pathname = useLocation({ select: (location) => location.pathname }); const isMacosDesktop = isElectron && isMacPlatform(navigator.platform); + const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth); + const sidebarMaximumWidth = resolveThreadSidebarMaximumWidth(window.innerWidth); const [isWindowFullscreen, setIsWindowFullscreen] = useState(() => { const getWindowFullscreenState = window.desktopBridge?.getWindowFullscreenState; return isMacosDesktop && typeof getWindowFullscreenState === "function" ? getWindowFullscreenState() : false; }); - const macosWindowControlsStyle = - isMacosDesktop && !isWindowFullscreen - ? ({ "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } as CSSProperties) - : undefined; + const sidebarProviderStyle = { + "--sidebar-width": `${sidebarWidth}px`, + ...(isMacosDesktop && !isWindowFullscreen + ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } + : {}), + } as CSSProperties; useEffect(() => { if (!isMacosDesktop) return; @@ -131,17 +153,19 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { }, [navigate, pathname]); return ( - + nextWidth <= currentWidth || wrapper.clientWidth - nextWidth >= THREAD_MAIN_CONTENT_MIN_WIDTH, storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, + onResize: setSidebarWidth, }} > diff --git a/apps/web/src/components/threadSidebarWidth.test.ts b/apps/web/src/components/threadSidebarWidth.test.ts new file mode 100644 index 00000000000..12aef63bd5e --- /dev/null +++ b/apps/web/src/components/threadSidebarWidth.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + resolveInitialThreadSidebarWidth, + THREAD_MAIN_CONTENT_MIN_WIDTH, + THREAD_SIDEBAR_DEFAULT_WIDTH, + THREAD_SIDEBAR_MIN_WIDTH, +} from "./threadSidebarWidth"; + +describe("thread sidebar width", () => { + it("uses the default width when no preference is stored", () => { + expect(resolveInitialThreadSidebarWidth(null, 1200)).toBe(THREAD_SIDEBAR_DEFAULT_WIDTH); + }); + + it("uses a stored width in the initial render", () => { + expect(resolveInitialThreadSidebarWidth(360, 1200)).toBe(360); + }); + + it("clamps a stored width to the sidebar minimum", () => { + expect(resolveInitialThreadSidebarWidth(120, 1200)).toBe(THREAD_SIDEBAR_MIN_WIDTH); + }); + + it("leaves enough room for the main content on a smaller window", () => { + const viewportWidth = 1000; + + expect(resolveInitialThreadSidebarWidth(900, viewportWidth)).toBe( + viewportWidth - THREAD_MAIN_CONTENT_MIN_WIDTH, + ); + }); + + it("keeps the sidebar minimum when the whole layout is narrower than its minimums", () => { + expect(resolveInitialThreadSidebarWidth(900, 700)).toBe(THREAD_SIDEBAR_MIN_WIDTH); + }); +}); diff --git a/apps/web/src/components/threadSidebarWidth.ts b/apps/web/src/components/threadSidebarWidth.ts new file mode 100644 index 00000000000..93dd196e84d --- /dev/null +++ b/apps/web/src/components/threadSidebarWidth.ts @@ -0,0 +1,22 @@ +export const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width"; +export const THREAD_SIDEBAR_DEFAULT_WIDTH = 16 * 16; +export const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16; +export const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16; + +export function resolveThreadSidebarMaximumWidth(viewportWidth: number): number { + return Math.max( + THREAD_SIDEBAR_MIN_WIDTH, + Math.floor(viewportWidth) - THREAD_MAIN_CONTENT_MIN_WIDTH, + ); +} + +export function resolveInitialThreadSidebarWidth( + storedWidth: number | null, + viewportWidth: number, +): number { + const preferredWidth = + storedWidth === null + ? THREAD_SIDEBAR_DEFAULT_WIDTH + : Math.max(THREAD_SIDEBAR_MIN_WIDTH, storedWidth); + return Math.min(preferredWidth, resolveThreadSidebarMaximumWidth(viewportWidth)); +} diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 50fb652a495..51335d1a6f8 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -555,16 +555,24 @@ function SidebarRail({ [onClick, open, resolvedResizable, toggleSidebar], ); - React.useEffect(() => { + React.useLayoutEffect(() => { if (!resolvedResizable?.storageKey || typeof window === "undefined") return; const rail = railRef.current; if (!rail) return; const wrapper = rail.closest("[data-slot='sidebar-wrapper']"); if (!wrapper) return; - const storedWidth = getLocalStorageItem(resolvedResizable.storageKey, Schema.Finite); + let storedWidth: number | null; + try { + storedWidth = getLocalStorageItem(resolvedResizable.storageKey, Schema.Finite); + } catch (error) { + console.error("Could not restore persisted sidebar width.", error); + return; + } if (storedWidth === null) return; const clampedWidth = clampSidebarWidth(storedWidth, resolvedResizable); + // Hydrate the CSS variable before the browser paints so a restored sidebar + // never flashes at the default width first. wrapper.style.setProperty("--sidebar-width", `${clampedWidth}px`); resolvedResizable.onResize?.(clampedWidth); }, [resolvedResizable]); From db4b2d8a0f23b00f19774dda33e0b02627272c90 Mon Sep 17 00:00:00 2001 From: Rusiru Sadathana Date: Mon, 20 Jul 2026 18:14:58 +0530 Subject: [PATCH 32/35] perf(orchestration): speed up new-chat propagation and offline catch-up (#4177) Co-authored-by: codex Co-authored-by: Julius Marminge --- .../Layers/OrchestrationEngine.test.ts | 2 + .../Layers/OrchestrationEngine.ts | 5 + .../Services/OrchestrationEngine.ts | 7 + .../src/relay/AgentAwarenessRelay.test.ts | 2 + apps/server/src/server.test.ts | 470 +++++++++++++++++- apps/server/src/serverRuntimeStartup.test.ts | 3 + apps/server/src/ws.ts | 356 +++++++++---- 7 files changed, 749 insertions(+), 96 deletions(-) diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 405103450e8..b00c00e0d3f 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -218,6 +218,7 @@ describe("OrchestrationEngine", () => { const runtime = ManagedRuntime.make(layer); const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); + expect(await runtime.runPromise(engine.latestSequence)).toBe(7); const result = await runtime.runPromise( engine.dispatch({ type: "thread.meta.update", @@ -228,6 +229,7 @@ describe("OrchestrationEngine", () => { ); expect(result.sequence).toBe(8); + expect(await runtime.runPromise(engine.latestSequence)).toBe(8); expect(fullSnapshotReadCount).toBe(0); await runtime.dispose(); diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts index 9598d1c3ef5..19184915ac7 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts @@ -329,6 +329,11 @@ const makeOrchestrationEngine = Effect.gen(function* () { get streamDomainEvents(): OrchestrationEngineShape["streamDomainEvents"] { return Stream.fromPubSub(eventPubSub); }, + // The command read model's snapshotSequence tracks the latest committed + // event sequence (updated on the worker fiber). A plain property read is a + // consistent, committed value — reassignment of `commandReadModel` is + // atomic on the single-threaded event loop. + latestSequence: Effect.sync(() => commandReadModel.snapshotSequence), } satisfies OrchestrationEngineShape; }); diff --git a/apps/server/src/orchestration/Services/OrchestrationEngine.ts b/apps/server/src/orchestration/Services/OrchestrationEngine.ts index dc1b5fd3003..f8bcfd76ac0 100644 --- a/apps/server/src/orchestration/Services/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Services/OrchestrationEngine.ts @@ -56,6 +56,13 @@ export interface OrchestrationEngineShape { * This is a hot runtime stream (new events only), not a historical replay. */ readonly streamDomainEvents: Stream.Stream; + + /** + * The latest sequence reflected in the engine's authoritative command read + * model (0 if none). Used to gauge how far behind a resuming client is before + * choosing between an incremental replay and a fresh projected snapshot. + */ + readonly latestSequence: Effect.Effect; } /** diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts index 9ca17dbc2dd..0c261c8f21e 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts @@ -470,6 +470,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { readEvents: () => Stream.empty, dispatch: () => Effect.succeed({ sequence: 1 }), streamDomainEvents: Stream.fromQueue(events), + latestSequence: Effect.succeed(0), } satisfies OrchestrationEngineShape; const snapshotQuery = { @@ -659,6 +660,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { readEvents: () => Stream.empty, dispatch: () => Effect.succeed({ sequence: 1 }), streamDomainEvents: Stream.fromQueue(events), + latestSequence: Effect.succeed(0), } satisfies OrchestrationEngineShape), Layer.succeed(ProjectionSnapshotQuery, { getShellSnapshot: () => diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index a0f41cbf0fc..6122c498145 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -46,6 +46,7 @@ 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 Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as ManagedRuntime from "effect/ManagedRuntime"; import * as Option from "effect/Option"; @@ -677,6 +678,7 @@ const buildAppUnderTest = (options?: { readEvents: () => Stream.empty, dispatch: () => Effect.succeed({ sequence: 0 }), streamDomainEvents: Stream.empty, + latestSequence: Effect.succeed(0), ...options?.layers?.orchestrationEngine, }), ), @@ -5688,7 +5690,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => { projectionSnapshotQuery: { getShellSnapshot: () => Effect.gen(function* () { - yield* Effect.sleep("25 millis"); yield* PubSub.publish(liveEvents, deletedEvent); return { snapshotSequence: 1, @@ -5774,6 +5775,473 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), ); + it.effect("subscribeShell sends a fresh snapshot instead of replaying a large gap", () => + Effect.gen(function* () { + let readEventsCalls = 0; + const snapshotThreadId = ThreadId.make("thread-from-snapshot"); + const now = "2026-01-01T00:00:00.000Z"; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + // Head is far ahead of the client's afterSequence (gap > 1000). + latestSequence: Effect.succeed(100_000), + readEvents: () => + Stream.sync(() => { + readEventsCalls += 1; + return { + sequence: 1, + eventId: EventId.make("event-should-not-be-read"), + aggregateKind: "thread", + aggregateId: snapshotThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.created", + payload: {} as never, + } satisfies OrchestrationEvent; + }), + }, + projectionSnapshotQuery: { + getShellSnapshot: () => + Effect.succeed({ + snapshotSequence: 100_000, + projects: [], + threads: [makeDefaultOrchestrationThreadShell({ id: snapshotThreadId })], + updatedAt: now, + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ + afterSequence: 5, + requestCompletionMarker: true, + }).pipe(Stream.take(2), Stream.runCollect), + ), + ); + + const [first, second] = Array.from(items); + // Large gap => fresh snapshot, and the unbounded replay is never started. + assert.equal(first?.kind, "snapshot"); + if (first?.kind === "snapshot") { + assert.equal(first.snapshot.threads[0]?.id, snapshotThreadId); + } + assert.equal(second?.kind, "synchronized"); + assert.equal(readEventsCalls, 0); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeShell replaces a cursor ahead of the authoritative head", () => + Effect.gen(function* () { + let readEventsCalls = 0; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(5), + readEvents: () => + Stream.sync(() => { + readEventsCalls += 1; + return {} as OrchestrationEvent; + }), + }, + projectionSnapshotQuery: { + getShellSnapshot: () => + Effect.succeed({ + snapshotSequence: 5, + projects: [], + threads: [], + updatedAt: "2026-01-01T00:00:00.000Z", + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const first = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ afterSequence: 10 }).pipe( + Stream.runHead, + ), + ), + ); + + assert.equal(Option.getOrThrow(first).kind, "snapshot"); + assert.equal(readEventsCalls, 0); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeShell coalesces a per-thread burst without stalling other threads", () => + Effect.gen(function* () { + const busyThreadId = ThreadId.make("thread-busy"); + const newThreadId = ThreadId.make("thread-new"); + const now = "2026-01-01T00:00:00.000Z"; + const shellFetches: Array = []; + let replayLimit: number | undefined; + + const messageEvent = (sequence: number): OrchestrationEvent => + ({ + sequence, + eventId: EventId.make(`event-${sequence}`), + aggregateKind: "thread", + aggregateId: busyThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.message-sent", + payload: {} as never, + }) satisfies OrchestrationEvent; + + const createdEvent: OrchestrationEvent = { + sequence: 50, + eventId: EventId.make("event-created"), + aggregateKind: "thread", + aggregateId: newThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.created", + payload: {} as never, + }; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(50), + // A burst of message-sent deltas for the busy thread, plus one + // thread.created for a different thread, all within one batch. + readEvents: (_afterSequence, limit) => { + replayLimit = limit; + return Stream.fromIterable([ + ...Array.from({ length: 20 }, (_unused, index) => messageEvent(index + 1)), + createdEvent, + ]); + }, + }, + projectionSnapshotQuery: { + getThreadShellById: (threadId) => + Effect.sync(() => { + shellFetches.push(threadId); + return Option.some(makeDefaultOrchestrationThreadShell({ id: threadId })); + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ + afterSequence: 0, + requestCompletionMarker: true, + }).pipe(Stream.take(3), Stream.runCollect), + ), + ); + + const collected = Array.from(items); + const upsertedIds = collected.flatMap((item) => + item.kind === "thread-upserted" ? [item.thread.id] : [], + ); + // Both threads surface, and the busy thread's 20-event burst collapses to + // a single shell refetch (not 20). The new thread is not stuck behind it. + assert.include(upsertedIds, busyThreadId); + assert.include(upsertedIds, newThreadId); + assert.equal(collected[2]?.kind, "synchronized"); + assert.equal(shellFetches.filter((id) => id === busyThreadId).length, 1); + assert.equal(replayLimit, 50); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeShell coalesces live bursts after the synchronization marker", () => + Effect.gen(function* () { + const busyThreadId = ThreadId.make("thread-live-busy"); + const newThreadId = ThreadId.make("thread-live-new"); + const now = "2026-01-01T00:00:00.000Z"; + const liveEvents = yield* PubSub.unbounded(); + const synchronized = yield* Deferred.make(); + const shellFetches: Array = []; + const observedLiveThreadIds = new Set(); + + const messageEvent = (sequence: number): OrchestrationEvent => + ({ + sequence, + eventId: EventId.make(`event-live-${sequence}`), + aggregateKind: "thread", + aggregateId: busyThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.message-sent", + payload: {} as never, + }) satisfies OrchestrationEvent; + + const createdEvent: OrchestrationEvent = { + sequence: 50, + eventId: EventId.make("event-live-created"), + aggregateKind: "thread", + aggregateId: newThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.created", + payload: {} as never, + }; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + streamDomainEvents: Stream.fromPubSub(liveEvents), + }, + projectionSnapshotQuery: { + getThreadShellById: (threadId) => + Effect.sync(() => { + shellFetches.push(threadId); + return Option.some(makeDefaultOrchestrationThreadShell({ id: threadId })); + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + Effect.gen(function* () { + const itemsFiber = yield* withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ + requestCompletionMarker: true, + }).pipe( + Stream.tap((item) => + item.kind === "synchronized" + ? Deferred.succeed(synchronized, undefined).pipe(Effect.ignore) + : Effect.void, + ), + Stream.takeUntil((item) => { + if (item.kind === "thread-upserted") { + observedLiveThreadIds.add(item.thread.id); + } + return ( + observedLiveThreadIds.has(busyThreadId) && observedLiveThreadIds.has(newThreadId) + ); + }), + Stream.runCollect, + ), + ).pipe(Effect.forkScoped); + + yield* Deferred.await(synchronized); + for (const event of [ + ...Array.from({ length: 20 }, (_unused, index) => messageEvent(index + 1)), + createdEvent, + ]) { + yield* PubSub.publish(liveEvents, event); + } + + return yield* Fiber.join(itemsFiber); + }), + ).pipe(Effect.timeout("2 seconds")); + + assert.equal(items[0]?.kind, "snapshot"); + assert.equal(items[1]?.kind, "synchronized"); + const liveUpsertedIds = Array.from(items) + .slice(2) + .flatMap((item) => (item.kind === "thread-upserted" ? [item.thread.id] : [])); + assert.include(liveUpsertedIds, busyThreadId); + assert.include(liveUpsertedIds, newThreadId); + assert.isBelow(shellFetches.filter((id) => id === busyThreadId).length, 20); + }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), + ); + + it.effect("subscribeShell coalescing still emits a removal for a deleted thread", () => + Effect.gen(function* () { + const goneThreadId = ThreadId.make("thread-gone"); + const now = "2026-01-01T00:00:00.000Z"; + + const makeThreadEvent = ( + sequence: number, + type: "thread.deleted" | "thread.message-sent", + ): OrchestrationEvent => + ({ + sequence, + eventId: EventId.make(`event-${sequence}`), + aggregateKind: "thread", + aggregateId: goneThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type, + payload: type === "thread.deleted" ? { threadId: goneThreadId, deletedAt: now } : {}, + }) as OrchestrationEvent; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(2), + // A thread.deleted followed, within the same coalescing window, by a + // later refetchable event for the same thread. The later event wins + // coalescing; its shell refetch returns none (the row is gone), which + // must still surface a removal rather than be swallowed. + readEvents: () => + Stream.fromIterable([ + makeThreadEvent(1, "thread.deleted"), + makeThreadEvent(2, "thread.message-sent"), + ]), + }, + projectionSnapshotQuery: { + getThreadShellById: () => Effect.succeed(Option.none()), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ afterSequence: 0 }).pipe( + Stream.take(1), + Stream.runCollect, + ), + ), + ); + + const [first] = Array.from(items); + assert.equal(first?.kind, "thread-removed"); + assert.equal(first?.kind === "thread-removed" ? first.threadId : null, goneThreadId); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeShell retries a transient shell projection refetch failure", () => + Effect.gen(function* () { + const threadId = ThreadId.make("thread-transient-refetch"); + const now = "2026-01-01T00:00:00.000Z"; + let attempts = 0; + + const event: OrchestrationEvent = { + sequence: 1, + eventId: EventId.make("event-transient-refetch"), + aggregateKind: "thread", + aggregateId: threadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.message-sent", + payload: {} as never, + }; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(1), + readEvents: () => Stream.make(event), + }, + projectionSnapshotQuery: { + getThreadShellById: () => + Effect.suspend(() => { + attempts += 1; + return attempts === 1 + ? Effect.fail( + new PersistenceSqlError({ + operation: "test.shell-refetch", + detail: "transient failure", + }), + ) + : Effect.succeed( + Option.some(makeDefaultOrchestrationThreadShell({ id: threadId })), + ); + }), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ afterSequence: 0 }).pipe( + Stream.take(1), + Stream.runCollect, + ), + ), + ); + + const [first] = Array.from(items); + assert.equal(first?.kind, "thread-upserted"); + assert.equal(first?.kind === "thread-upserted" ? first.thread.id : null, threadId); + assert.equal(attempts, 2); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeShell coalescing still removes a project after a trailing update", () => + Effect.gen(function* () { + const projectId = ProjectId.make("project-gone"); + const now = "2026-01-01T00:00:00.000Z"; + + const makeProjectEvent = ( + sequence: number, + type: "project.deleted" | "project.meta-updated", + ): OrchestrationEvent => + ({ + sequence, + eventId: EventId.make(`event-project-${sequence}`), + aggregateKind: "project", + aggregateId: projectId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type, + payload: + type === "project.deleted" + ? { projectId, deletedAt: now } + : { projectId, title: "Still deleted", updatedAt: now }, + }) as OrchestrationEvent; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(2), + readEvents: () => + Stream.fromIterable([ + makeProjectEvent(1, "project.deleted"), + makeProjectEvent(2, "project.meta-updated"), + ]), + }, + projectionSnapshotQuery: { + getProjectShellById: () => Effect.succeed(Option.none()), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeShell]({ afterSequence: 0 }).pipe( + Stream.take(1), + Stream.runCollect, + ), + ), + ); + + const [first] = Array.from(items); + assert.equal(first?.kind, "project-removed"); + assert.equal(first?.kind === "project-removed" ? first.projectId : null, projectId); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("enriches replayed project events with repository identity metadata", () => Effect.gen(function* () { const repositoryIdentity = { diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index 1a331bc717f..b8102bda9ad 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -168,6 +168,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, + latestSequence: Effect.succeed(0), } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provide(NodeServices.layer), ); @@ -211,6 +212,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, + latestSequence: Effect.succeed(0), } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provide(NodeServices.layer), ); @@ -260,6 +262,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa Effect.as({ sequence: 1 }), ), streamDomainEvents: Stream.empty, + latestSequence: Effect.succeed(0), } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]), Effect.provideService(Crypto.Crypto, { ...crypto, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 41c29fc3388..08b1770a0a2 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -36,6 +36,7 @@ import { OrchestrationGetSnapshotError, OrchestrationGetTurnDiffError, ORCHESTRATION_WS_METHODS, + type ProjectId, type ProjectEntriesFailure, type ProjectFileFailure, type ProjectFileOperation, @@ -276,6 +277,13 @@ function isThreadDetailEvent(event: OrchestrationEvent): event is Extract< const PROVIDER_STATUS_DEBOUNCE_MS = 200; +// When a resuming client's cursor is more than this many events behind the +// current head, skip the per-event catch-up replay and send a fresh shell +// snapshot instead. Replaying each intervening event costs a shell refetch; +// past this gap a single O(active-threads) snapshot is cheaper and bounded. +// Matches the event store's default page size (DEFAULT_READ_FROM_SEQUENCE_LIMIT). +const SHELL_RESUME_MAX_GAP = 1_000; + const RPC_REQUIRED_SCOPE = new Map([ [ORCHESTRATION_WS_METHODS.dispatchCommand, AuthOrchestrationOperateScope], [ORCHESTRATION_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope], @@ -621,16 +629,7 @@ const makeWsRpcLayer = ( switch (event.type) { case "project.created": case "project.meta-updated": - return projectionSnapshotQuery.getProjectShellById(event.payload.projectId).pipe( - Effect.map((project) => - Option.map(project, (nextProject) => ({ - kind: "project-upserted" as const, - sequence: event.sequence, - project: nextProject, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); + return projectUpsertOrRemove(event.payload.projectId, event.sequence); case "project.deleted": return Effect.succeed( Option.some({ @@ -649,35 +648,192 @@ const makeWsRpcLayer = ( }), ); case "thread.unarchived": - return projectionSnapshotQuery.getThreadShellById(event.payload.threadId).pipe( - Effect.map((thread) => - Option.map(thread, (nextThread) => ({ - kind: "thread-upserted" as const, - sequence: event.sequence, - thread: nextThread, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); + return threadUpsertOrRemove(event.payload.threadId, event.sequence); default: if (event.aggregateKind !== "thread") { return Effect.succeed(Option.none()); } - return projectionSnapshotQuery - .getThreadShellById(ThreadId.make(event.aggregateId)) - .pipe( - Effect.map((thread) => - Option.map(thread, (nextThread) => ({ - kind: "thread-upserted" as const, - sequence: event.sequence, - thread: nextThread, - })), - ), - Effect.orElseSucceed(() => Option.none()), - ); + return threadUpsertOrRemove(ThreadId.make(event.aggregateId), event.sequence); } }; + // Coalescing makes each projection read represent every event for that + // aggregate in the current window. Retry a typed persistence failure once + // so a brief read failure cannot strand the shell at its previous state. + // If both attempts fail, log and drop the stream item; treating an error as + // a missing row would incorrectly remove a still-active aggregate. + const retryShellProjectionRead = ( + aggregateKind: "project" | "thread", + aggregateId: string, + read: Effect.Effect, + ): Effect.Effect, never, never> => + read.pipe( + Effect.retry({ times: 1 }), + Effect.map(Option.some), + Effect.tapError((error) => + Effect.logWarning("orchestration shell projection refetch failed", { + aggregateKind, + aggregateId, + error, + }), + ), + Effect.orElseSucceed(() => Option.none()), + ); + + const projectUpsertOrRemove = ( + projectId: ProjectId, + sequence: number, + ): Effect.Effect, never, never> => + retryShellProjectionRead( + "project", + projectId, + projectionSnapshotQuery.getProjectShellById(projectId), + ).pipe( + Effect.map( + Option.flatMap((project) => + Option.match(project, { + onNone: () => + Option.some({ + kind: "project-removed" as const, + sequence, + projectId, + }), + onSome: (nextProject) => + Option.some({ + kind: "project-upserted" as const, + sequence, + project: nextProject, + }), + }), + ), + ), + ); + + // Refetch a thread's shell and emit an upsert if it is still active, or a + // `thread-removed` if the projection has no active row for it. Emitting a + // removal on a `none` (rather than dropping the event) is what keeps + // coalescing correct: when a burst collapses a `thread.deleted`/`archived` + // into a later refetchable event for the same thread, the refetch returns + // `none` for the now-inactive row and this still tells the sidebar to drop + // it. A `thread-removed` the client does not have is a harmless no-op. The + // projection commits in the same transaction before the event publishes, + // so a `none` reliably means the thread is deleted or archived, not + // not-yet-persisted. + const threadUpsertOrRemove = ( + threadId: ThreadId, + sequence: number, + ): Effect.Effect, never, never> => + retryShellProjectionRead( + "thread", + threadId, + projectionSnapshotQuery.getThreadShellById(threadId), + ).pipe( + Effect.map( + Option.flatMap((thread) => + Option.match(thread, { + onNone: () => + Option.some({ + kind: "thread-removed" as const, + sequence, + threadId, + }), + onSome: (nextThread) => + Option.some({ + kind: "thread-upserted" as const, + sequence, + thread: nextThread, + }), + }), + ), + ), + ); + + // Turn a batch of domain events into shell stream items, coalescing by + // aggregate first. `toShellStreamEvent` re-reads the *current* projected + // shell for an aggregate, so within a batch only the latest event per + // aggregate matters: a burst of streaming `thread.message-sent` deltas for + // one thread collapses into a single shell refetch, and an unrelated + // `thread.created` in the same batch is never stuck behind those DB reads. + // + // Input events arrive in ascending sequence; we keep the last (highest + // sequence) event per aggregate, then re-sort ascending before emitting so + // the client — which applies shell items strictly by increasing sequence + // and drops any `sequence <= snapshotSequence` — never skips a coalesced + // item. The refetch runs with bounded concurrency (order-preserving). + const SHELL_REFETCH_CONCURRENCY = 8; + const coalesceShellEvents = ( + events: ReadonlyArray, + ): Effect.Effect, never, never> => + Effect.gen(function* () { + if (events.length === 0) { + return []; + } + const latestByAggregate = new Map(); + for (const event of events) { + latestByAggregate.set(`${event.aggregateKind}:${event.aggregateId}`, event); + } + const survivors = Array.from(latestByAggregate.values()).sort( + (left, right) => left.sequence - right.sequence, + ); + const shellEvents = yield* Effect.forEach(survivors, toShellStreamEvent, { + concurrency: SHELL_REFETCH_CONCURRENCY, + }); + return shellEvents.flatMap((option) => (Option.isSome(option) ? [option.value] : [])); + }); + + // Small time/size window over which to coalesce shell events. The window + // bounds the worst-case added latency for a brand-new thread to appear in + // the sidebar (imperceptible), while collapsing high-frequency streaming + // traffic so it can't serialize the shell stream behind per-event DB reads. + const SHELL_COALESCE_WINDOW = Duration.millis(50); + const SHELL_COALESCE_MAX_CHUNK = 512; + const coalesceShellStream = ( + stream: Stream.Stream, + ): Stream.Stream => + stream.pipe( + Stream.groupedWithin(SHELL_COALESCE_MAX_CHUNK, SHELL_COALESCE_WINDOW), + Stream.mapEffect(coalesceShellEvents), + Stream.flatMap((items) => Stream.fromIterable(items)), + ); + + type ShellLiveInput = + | { readonly kind: "event"; readonly event: OrchestrationEvent } + | { readonly kind: "synchronized" }; + + // A completion marker is queued alongside raw live events so it cannot + // overtake an event still waiting in the coalescing window. Split each + // batch at markers and coalesce only the event segments on either side. + const coalesceShellLiveInputs = ( + inputs: ReadonlyArray, + ): Effect.Effect, never, never> => + Effect.gen(function* () { + const output: Array = []; + let pendingEvents: Array = []; + + for (const input of inputs) { + if (input.kind === "event") { + pendingEvents.push(input.event); + continue; + } + + output.push(...(yield* coalesceShellEvents(pendingEvents))); + pendingEvents = []; + output.push({ kind: "synchronized" }); + } + + output.push(...(yield* coalesceShellEvents(pendingEvents))); + return output; + }); + + const coalesceShellLiveStream = ( + stream: Stream.Stream, + ): Stream.Stream => + stream.pipe( + Stream.groupedWithin(SHELL_COALESCE_MAX_CHUNK, SHELL_COALESCE_WINDOW), + Stream.mapEffect(coalesceShellLiveInputs), + Stream.flatMap((items) => Stream.fromIterable(items)), + ); + const dispatchBootstrapTurnStart = ( command: Extract, ): Effect.Effect<{ readonly sequence: number }, OrchestrationDispatchCommandError> => @@ -1068,68 +1224,29 @@ const makeWsRpcLayer = ( observeRpcStreamEffect( ORCHESTRATION_WS_METHODS.subscribeShell, Effect.gen(function* () { - const liveStream = orchestrationEngine.streamDomainEvents.pipe( - Stream.mapEffect(toShellStreamEvent), - Stream.flatMap((event) => - Option.isSome(event) ? Stream.succeed(event.value) : Stream.empty, + // Coalesce the live shell stream per aggregate over a small window + // so bursts of high-frequency events (streaming message deltas, + // activity appends) collapse into a single shell refetch and never + // serialize a brand-new thread's `thread.created` behind hundreds + // of per-event DB reads. See coalesceShellStream. + // Attach live delivery into a scope-bound buffer BEFORE loading any + // snapshot or draining catch-up, otherwise an event published while + // the snapshot query is in flight is lost (it is past the snapshot's + // sequence but the live subscription is not attached yet). Every + // path below emits from this same buffered live tail. Overlapping + // events are deduped by sequence on the client. + const liveBuffer = yield* Queue.unbounded(); + yield* Effect.forkScoped( + orchestrationEngine.streamDomainEvents.pipe( + Stream.runForEach((event) => + Queue.offer(liveBuffer, { kind: "event" as const, event }), + ), ), + { startImmediately: true }, ); + const bufferedLiveStream = coalesceShellLiveStream(Stream.fromQueue(liveBuffer)); - // When the client already holds a shell snapshot (cached, or loaded - // over HTTP) it passes that snapshot's sequence, and we resume by - // replaying shell events after it instead of re-sending the whole - // projects/threads list over the socket. As in the thread path, the - // live subscription is attached (into a scope-bound buffer) before - // draining the catch-up replay so no event published during the - // replay window is lost; overlapping events are deduped by sequence - // on the client. The full range is read (not the store's default - // page limit) since the shell filter runs after reading. - if (input.afterSequence !== undefined) { - const afterSequence = input.afterSequence; - return Stream.unwrap( - Effect.gen(function* () { - const liveBuffer = yield* Queue.unbounded(); - yield* Effect.forkScoped( - liveStream.pipe(Stream.runForEach((item) => Queue.offer(liveBuffer, item))), - ); - const catchUpStream = orchestrationEngine - .readEvents(afterSequence, Number.MAX_SAFE_INTEGER) - .pipe( - Stream.mapEffect(toShellStreamEvent), - Stream.flatMap((event) => - Option.isSome(event) ? Stream.succeed(event.value) : Stream.empty, - ), - Stream.mapError( - (cause) => - new OrchestrationGetSnapshotError({ - message: "Failed to replay orchestration shell events", - cause, - }), - ), - ); - 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( + const loadSnapshot = projectionSnapshotQuery.getShellSnapshot().pipe( Effect.tapError((cause) => Effect.logError("orchestration shell snapshot load failed", { cause }), ), @@ -1142,21 +1259,70 @@ const makeWsRpcLayer = ( ), ); - const afterSnapshot = + // Offer the completion marker into the same queue as live events. + // Anything buffered while snapshot/replay work was in flight is + // therefore delivered before the client is told it is synchronized. + const synchronizedThenLive = input.requestCompletionMarker === true ? Stream.concat( Stream.fromEffect( - Queue.offer(liveBuffer, { kind: "synchronized" as const }), - ).pipe(Stream.drain), + Queue.offer(liveBuffer, { kind: "synchronized" as const }).pipe( + Effect.andThen(Queue.takeAll(liveBuffer)), + Effect.flatMap(coalesceShellLiveInputs), + ), + ).pipe(Stream.flatMap((items) => Stream.fromIterable(items))), bufferedLiveStream, ) : bufferedLiveStream; + + // When the client already holds a shell snapshot (cached, or loaded + // over HTTP) it passes that snapshot's sequence, and we resume by + // replaying shell events after it instead of re-sending the whole + // projects/threads list over the socket. If the client is too far + // behind, we fall back to a fresh snapshot instead of an unbounded + // replay (see below). + if (input.afterSequence !== undefined) { + const afterSequence = input.afterSequence; + const headSequence = yield* orchestrationEngine.latestSequence; + const replayGap = headSequence - afterSequence; + // Gap too large: replaying every intervening event (each a shell + // refetch) is far more expensive than a single O(active-threads) + // snapshot. A cursor ahead of this engine's authoritative state + // is also invalid, so reset it with a snapshot. Send the snapshot + // followed by the buffered live tail, exactly as the + // no-afterSequence path does. + if (replayGap < 0 || replayGap > SHELL_RESUME_MAX_GAP) { + const snapshot = yield* loadSnapshot; + return Stream.concat( + Stream.make({ kind: "snapshot" as const, snapshot }), + synchronizedThenLive, + ); + } + const catchUpStream = coalesceShellStream( + // Replay only through the head captured above. Newer events + // are already covered by the live subscription, so this bound + // cannot chase a moving event-store head or grow the live + // buffer indefinitely while waiting for an empty page. + orchestrationEngine.readEvents(afterSequence, replayGap), + ).pipe( + Stream.mapError( + (cause) => + new OrchestrationGetSnapshotError({ + message: "Failed to replay orchestration shell events", + cause, + }), + ), + ); + return Stream.concat(catchUpStream, synchronizedThenLive); + } + + const snapshot = yield* loadSnapshot; return Stream.concat( Stream.make({ kind: "snapshot" as const, snapshot, }), - afterSnapshot, + synchronizedThenLive, ); }), { "rpc.aggregate": "orchestration" }, From c8a04bd59b7e1a18124468f1592b4c925839a9de Mon Sep 17 00:00:00 2001 From: ss <69873514+sandersonstabo@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:45:36 +0200 Subject: [PATCH 33/35] Finale: upgrade changed files card to fix various UI issues (#4113) Co-authored-by: Julius Marminge --- .../components/chat/ChangedFilesTree.test.tsx | 27 +++++- .../src/components/chat/ChangedFilesTree.tsx | 96 ++++++++++++------- .../components/chat/MessagesTimeline.test.tsx | 54 ++++++++++- .../src/components/chat/MessagesTimeline.tsx | 84 +++++++++++----- 4 files changed, 201 insertions(+), 60 deletions(-) diff --git a/apps/web/src/components/chat/ChangedFilesTree.test.tsx b/apps/web/src/components/chat/ChangedFilesTree.test.tsx index c371acdb362..120827b34b2 100644 --- a/apps/web/src/components/chat/ChangedFilesTree.test.tsx +++ b/apps/web/src/components/chat/ChangedFilesTree.test.tsx @@ -2,7 +2,32 @@ import { TurnId } from "@t3tools/contracts"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vite-plus/test"; -import { ChangedFilesTree } from "./ChangedFilesTree"; +import { ChangedFilesCard, ChangedFilesTree } from "./ChangedFilesTree"; + +describe("ChangedFilesCard", () => { + it("keeps its compact header sticky while preserving singular labels", () => { + const markup = renderToStaticMarkup( + {}} + onOpenTurnDiff={() => {}} + />, + ); + + expect(markup).toContain('class="sticky top-0 z-10'); + expect(markup).not.toContain("self-start"); + expect(markup).toContain("whitespace-nowrap"); + expect(markup).toContain("!size-[22px]"); + expect(markup).toContain("size-3"); + expect(markup).toContain('aria-label="Collapse all"'); + expect(markup).toContain('aria-label="View diff"'); + expect(markup).toContain("1 changed file"); + expect(markup).not.toContain("1 changed files"); + }); +}); describe("ChangedFilesTree", () => { it.each([ diff --git a/apps/web/src/components/chat/ChangedFilesTree.tsx b/apps/web/src/components/chat/ChangedFilesTree.tsx index 3ea0e6315fd..6fc1462c5e6 100644 --- a/apps/web/src/components/chat/ChangedFilesTree.tsx +++ b/apps/web/src/components/chat/ChangedFilesTree.tsx @@ -6,11 +6,19 @@ import { summarizeTurnDiffStats, type TurnDiffTreeNode, } from "../../lib/turnDiffTree"; -import { ChevronRightIcon, FolderIcon, FolderClosedIcon } from "lucide-react"; +import { + ChevronsDownUpIcon, + ChevronsUpDownIcon, + ChevronRightIcon, + FileDiffIcon, + FolderIcon, + FolderClosedIcon, +} from "lucide-react"; import { cn } from "~/lib/utils"; import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { PierreEntryIcon } from "./PierreEntryIcon"; import { Button } from "../ui/button"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; const EMPTY_DIRECTORY_OVERRIDES: Record = {}; @@ -33,10 +41,12 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: { const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]); return ( -
-
-

- {files.length} changed files +

+
+

+ + {files.length} changed file{files.length === 1 ? "" : "s"} + {hasNonZeroStat(summaryStat) && (

- - + + + } + > + {allDirectoriesExpanded ? ( + + ) : ( + + )} + + + {allDirectoriesExpanded ? "Collapse all" : "Expand all"} + + + + onOpenTurnDiff(turnId, files[0]?.path)} + /> + } + > + + + View diff +
-
- -
+
); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index a58724f3308..b340a248fbe 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -1,4 +1,4 @@ -import { EnvironmentId, MessageId } from "@t3tools/contracts"; +import { CheckpointRef, EnvironmentId, MessageId, TurnId } from "@t3tools/contracts"; import { createRef, type ReactNode, type Ref } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { beforeAll, describe, expect, it, vi } from "vite-plus/test"; @@ -219,6 +219,58 @@ function buildUserTimelineEntry(text: string) { } describe("MessagesTimeline", () => { + it("keeps assistant changed-files headers sticky below the thread header", async () => { + const { MessagesTimeline } = await import("./MessagesTimeline"); + const assistantMessageId = MessageId.make("message-assistant-with-files"); + const turnId = TurnId.make("turn-with-files"); + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain('class="sticky top-2 z-10'); + expect(markup).not.toContain("self-start"); + expect(markup).toContain("whitespace-nowrap"); + expect(markup).toContain("!size-[22px]"); + expect(markup).toContain("size-3"); + expect(markup).toContain('aria-label="Collapse all"'); + expect(markup).toContain('aria-label="View diff"'); + expect(markup).toContain("1 changed file"); + }); + it("uses LegendList isNearEnd when deciding whether the live edge is visible", async () => { const { resolveTimelineIsAtEnd, diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 73fc86312e5..f759aa150be 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -44,8 +44,11 @@ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, + ChevronsDownUpIcon, + ChevronsUpDownIcon, CircleAlertIcon, EyeIcon, + FileDiffIcon, GlobeIcon, HammerIcon, MessageCircleIcon, @@ -1274,38 +1277,67 @@ function AssistantChangedFilesSectionInner({ ); const setExpanded = useUiStateStore((store) => store.setThreadChangedFilesExpanded); const summaryStat = summarizeTurnDiffStats(checkpointFiles); - const changedFileCountLabel = String(checkpointFiles.length); return ( -
-
-

- Changed files ({changedFileCountLabel}) +

+
+

+ + {checkpointFiles.length} changed file{checkpointFiles.length === 1 ? "" : "s"} + {hasNonZeroStat(summaryStat) && ( - <> - - - + )}

- - + + + setExpanded(routeThreadKey, turnSummary.turnId, !allDirectoriesExpanded) + } + /> + } + > + {allDirectoriesExpanded ? ( + + ) : ( + + )} + + + {allDirectoriesExpanded ? "Collapse all" : "Expand all"} + + + + onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)} + /> + } + > + + + View diff +
Date: Mon, 20 Jul 2026 16:22:35 +0200 Subject: [PATCH 34/35] Pass CLI OAuth config to hosted web deploy (#4186) Co-authored-by: codex --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12ac0370c3b..668d1fcb59d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -788,6 +788,7 @@ jobs: env: T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} @@ -871,6 +872,7 @@ jobs: --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \ --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \ --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \ + --build-env "T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=${T3CODE_CLERK_CLI_OAUTH_CLIENT_ID:-}" \ --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=${T3CODE_RELAY_CLIENT_OTLP_TRACES_URL:-}" \ --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=${T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET:-}" \ From dc31f3fe45b5a83e2460400600e6e33c542c9d2a Mon Sep 17 00:00:00 2001 From: tarik02 Date: Mon, 20 Jul 2026 22:53:20 +0300 Subject: [PATCH 35/35] fix codex launch args env precedence --- apps/server/src/provider/Layers/CodexAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 8a9405444ca..e72a5c71288 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -1448,7 +1448,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( cwd: input.cwd ?? process.cwd(), binaryPath: codexConfig.binaryPath, environment, - launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, environment), + launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment), ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), ...(isCodexResumeCursorSchema(input.resumeCursor) ? { resumeCursor: input.resumeCursor }